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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import TraceModuleJaeger from './TraceModuleJaeger';
|
|
3
|
+
import { ModuleContext } from './ModuleRegistry';
|
|
4
|
+
|
|
5
|
+
interface TraceModuleJaegerWrapperProps {
|
|
6
|
+
context: ModuleContext;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TraceModuleJaegerWrapper: React.FC<TraceModuleJaegerWrapperProps> = ({ context }) => {
|
|
10
|
+
return <TraceModuleJaeger context={context} />;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default TraceModuleJaegerWrapper;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { moduleRegistry } from './ModuleRegistry';
|
|
2
2
|
import EventLogModuleWrapper from './EventLogModuleWrapper';
|
|
3
|
+
import TraceModuleJaegerWrapper from './TraceModuleJaegerWrapper';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Register all available modules
|
|
@@ -15,6 +16,15 @@ export function registerModules(): void {
|
|
|
15
16
|
component: EventLogModuleWrapper,
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
// Register Trace Viewer with Timeline, Statistics, RED Metrics, and Dependencies
|
|
20
|
+
moduleRegistry.register({
|
|
21
|
+
id: 'traceViewerJaeger',
|
|
22
|
+
name: 'Trace Viewer',
|
|
23
|
+
description: 'Timeline, Statistics, RED Metrics, and Dependencies',
|
|
24
|
+
icon: '🔎',
|
|
25
|
+
component: TraceModuleJaegerWrapper,
|
|
26
|
+
});
|
|
27
|
+
|
|
18
28
|
// Future modules can be registered here
|
|
19
29
|
// moduleRegistry.register({ ... });
|
|
20
30
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Select Component with Autocomplete
|
|
3
|
+
*
|
|
4
|
+
* Elegant multi-select dropdown with:
|
|
5
|
+
* - Autocomplete filtering
|
|
6
|
+
* - Tag-based selection display
|
|
7
|
+
* - Keyboard navigation
|
|
8
|
+
* - Remove on click
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
12
|
+
|
|
13
|
+
interface MultiSelectProps {
|
|
14
|
+
options: string[];
|
|
15
|
+
selected: string[];
|
|
16
|
+
onChange: (selected: string[]) => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
22
|
+
options,
|
|
23
|
+
selected,
|
|
24
|
+
onChange,
|
|
25
|
+
placeholder = 'Select items...',
|
|
26
|
+
disabled = false
|
|
27
|
+
}) => {
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
30
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
33
|
+
|
|
34
|
+
// Filter options based on search term and exclude already selected
|
|
35
|
+
const filteredOptions = options.filter(
|
|
36
|
+
option =>
|
|
37
|
+
!selected.includes(option) &&
|
|
38
|
+
option.toLowerCase().includes(searchTerm.toLowerCase())
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Close dropdown when clicking outside
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
44
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
45
|
+
setIsOpen(false);
|
|
46
|
+
setSearchTerm('');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
51
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const handleSelect = (option: string) => {
|
|
55
|
+
onChange([...selected, option]);
|
|
56
|
+
setSearchTerm('');
|
|
57
|
+
setFocusedIndex(0);
|
|
58
|
+
inputRef.current?.focus();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleRemove = (option: string) => {
|
|
62
|
+
onChange(selected.filter(item => item !== option));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
66
|
+
if (disabled) return;
|
|
67
|
+
|
|
68
|
+
switch (e.key) {
|
|
69
|
+
case 'ArrowDown':
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
setFocusedIndex(prev => Math.min(prev + 1, filteredOptions.length - 1));
|
|
72
|
+
break;
|
|
73
|
+
case 'ArrowUp':
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
setFocusedIndex(prev => Math.max(prev - 1, 0));
|
|
76
|
+
break;
|
|
77
|
+
case 'Enter':
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
if (filteredOptions[focusedIndex]) {
|
|
80
|
+
handleSelect(filteredOptions[focusedIndex]);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'Escape':
|
|
84
|
+
setIsOpen(false);
|
|
85
|
+
setSearchTerm('');
|
|
86
|
+
break;
|
|
87
|
+
case 'Backspace':
|
|
88
|
+
if (searchTerm === '' && selected.length > 0) {
|
|
89
|
+
const lastItem = selected[selected.length - 1];
|
|
90
|
+
if (lastItem) {
|
|
91
|
+
handleRemove(lastItem);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div ref={containerRef} style={{ position: 'relative' }}>
|
|
100
|
+
{/* Input Container with Tags */}
|
|
101
|
+
<div
|
|
102
|
+
onClick={() => !disabled && inputRef.current?.focus()}
|
|
103
|
+
style={{
|
|
104
|
+
minHeight: '38px',
|
|
105
|
+
padding: 'var(--space-xs)',
|
|
106
|
+
backgroundColor: disabled ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.3)',
|
|
107
|
+
border: `1px solid ${isOpen ? 'rgba(96, 165, 250, 0.5)' : 'rgba(255, 255, 255, 0.1)'}`,
|
|
108
|
+
borderRadius: 'var(--radius-sm)',
|
|
109
|
+
display: 'flex',
|
|
110
|
+
flexWrap: 'wrap',
|
|
111
|
+
gap: 'var(--gap-xs)',
|
|
112
|
+
cursor: disabled ? 'not-allowed' : 'text',
|
|
113
|
+
transition: 'border-color 0.2s',
|
|
114
|
+
opacity: disabled ? 0.6 : 1
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{/* Selected Tags */}
|
|
118
|
+
{selected.map(item => (
|
|
119
|
+
<div
|
|
120
|
+
key={item}
|
|
121
|
+
style={{
|
|
122
|
+
display: 'flex',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
gap: 'var(--gap-xs)',
|
|
125
|
+
padding: '2px 8px',
|
|
126
|
+
backgroundColor: 'rgba(96, 165, 250, 0.2)',
|
|
127
|
+
border: '1px solid rgba(96, 165, 250, 0.4)',
|
|
128
|
+
borderRadius: 'var(--radius-sm)',
|
|
129
|
+
fontSize: 'var(--font-size-sm)',
|
|
130
|
+
color: '#60a5fa',
|
|
131
|
+
maxWidth: '100%',
|
|
132
|
+
overflow: 'hidden'
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<span style={{
|
|
136
|
+
overflow: 'hidden',
|
|
137
|
+
textOverflow: 'ellipsis',
|
|
138
|
+
whiteSpace: 'nowrap',
|
|
139
|
+
minWidth: 0
|
|
140
|
+
}} title={item}>
|
|
141
|
+
{item}
|
|
142
|
+
</span>
|
|
143
|
+
{!disabled && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={(e) => {
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
handleRemove(item);
|
|
148
|
+
}}
|
|
149
|
+
style={{
|
|
150
|
+
background: 'none',
|
|
151
|
+
border: 'none',
|
|
152
|
+
color: '#60a5fa',
|
|
153
|
+
cursor: 'pointer',
|
|
154
|
+
padding: 0,
|
|
155
|
+
display: 'flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
fontSize: '14px',
|
|
158
|
+
lineHeight: 1,
|
|
159
|
+
flexShrink: 0
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
×
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
|
|
168
|
+
{/* Search Input */}
|
|
169
|
+
<input
|
|
170
|
+
ref={inputRef}
|
|
171
|
+
type="text"
|
|
172
|
+
value={searchTerm}
|
|
173
|
+
onChange={(e) => {
|
|
174
|
+
setSearchTerm(e.target.value);
|
|
175
|
+
setIsOpen(true);
|
|
176
|
+
setFocusedIndex(0);
|
|
177
|
+
}}
|
|
178
|
+
onFocus={() => !disabled && setIsOpen(true)}
|
|
179
|
+
onKeyDown={handleKeyDown}
|
|
180
|
+
placeholder={selected.length === 0 ? placeholder : ''}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
style={{
|
|
183
|
+
flex: 1,
|
|
184
|
+
minWidth: '120px',
|
|
185
|
+
background: 'transparent',
|
|
186
|
+
border: 'none',
|
|
187
|
+
outline: 'none',
|
|
188
|
+
color: 'var(--color-text-primary)',
|
|
189
|
+
fontSize: 'var(--font-size-base)',
|
|
190
|
+
padding: '4px'
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Dropdown Options */}
|
|
196
|
+
{isOpen && !disabled && filteredOptions.length > 0 && (
|
|
197
|
+
<div
|
|
198
|
+
style={{
|
|
199
|
+
position: 'absolute',
|
|
200
|
+
top: 'calc(100% + 4px)',
|
|
201
|
+
left: 0,
|
|
202
|
+
right: 0,
|
|
203
|
+
maxHeight: '240px',
|
|
204
|
+
overflowY: 'auto',
|
|
205
|
+
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
206
|
+
border: '1px solid rgba(96, 165, 250, 0.3)',
|
|
207
|
+
borderRadius: 'var(--radius-sm)',
|
|
208
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
|
|
209
|
+
zIndex: 1000
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{filteredOptions.map((option, index) => (
|
|
213
|
+
<div
|
|
214
|
+
key={option}
|
|
215
|
+
onClick={() => handleSelect(option)}
|
|
216
|
+
onMouseEnter={() => setFocusedIndex(index)}
|
|
217
|
+
style={{
|
|
218
|
+
padding: 'var(--space-xs) var(--space-sm)',
|
|
219
|
+
cursor: 'pointer',
|
|
220
|
+
backgroundColor: index === focusedIndex ? 'rgba(96, 165, 250, 0.2)' : 'transparent',
|
|
221
|
+
color: index === focusedIndex ? '#60a5fa' : 'var(--color-text-primary)',
|
|
222
|
+
fontSize: 'var(--font-size-sm)',
|
|
223
|
+
transition: 'background-color 0.15s'
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{option}
|
|
227
|
+
</div>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export default MultiSelect;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracing Settings Tab
|
|
3
|
+
*
|
|
4
|
+
* Configure OpenTelemetry tracing options:
|
|
5
|
+
* - Enable/disable auto-tracing
|
|
6
|
+
* - Service whitelist (FLOCK_TRACE_SERVICES)
|
|
7
|
+
* - Operation blacklist (FLOCK_TRACE_IGNORE)
|
|
8
|
+
* - Unified workflow tracing
|
|
9
|
+
* - Clear trace database
|
|
10
|
+
* - View trace statistics
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useState, useEffect } from 'react';
|
|
14
|
+
import MultiSelect from './MultiSelect';
|
|
15
|
+
|
|
16
|
+
interface TraceStats {
|
|
17
|
+
total_spans: number;
|
|
18
|
+
total_traces: number;
|
|
19
|
+
services_count: number;
|
|
20
|
+
oldest_trace: string | null;
|
|
21
|
+
newest_trace: string | null;
|
|
22
|
+
database_size_mb: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TraceServices {
|
|
26
|
+
services: string[];
|
|
27
|
+
operations: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TracingSettings: React.FC = () => {
|
|
31
|
+
// Tracing configuration state
|
|
32
|
+
const [autoTrace, setAutoTrace] = useState(false);
|
|
33
|
+
const [traceFile, setTraceFile] = useState(true);
|
|
34
|
+
const [autoWorkflow, setAutoWorkflow] = useState(false);
|
|
35
|
+
const [traceTtlDays, setTraceTtlDays] = useState<number | null>(30);
|
|
36
|
+
|
|
37
|
+
// Service/operation filter state
|
|
38
|
+
const [availableServices, setAvailableServices] = useState<string[]>([]);
|
|
39
|
+
const [availableOperations, setAvailableOperations] = useState<string[]>([]);
|
|
40
|
+
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
|
41
|
+
const [ignoredOperations, setIgnoredOperations] = useState<string[]>([]);
|
|
42
|
+
|
|
43
|
+
// UI state
|
|
44
|
+
const [stats, setStats] = useState<TraceStats | null>(null);
|
|
45
|
+
const [loading, setLoading] = useState(false);
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// Load initial data
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
loadTraceServices();
|
|
52
|
+
loadTraceStats();
|
|
53
|
+
loadCurrentSettings();
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const loadTraceServices = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch('/api/traces/services');
|
|
59
|
+
if (!response.ok) throw new Error('Failed to load trace services');
|
|
60
|
+
const data: TraceServices = await response.json();
|
|
61
|
+
setAvailableServices(data.services);
|
|
62
|
+
setAvailableOperations(data.operations);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('Error loading trace services:', err);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const loadTraceStats = async () => {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch('/api/traces/stats');
|
|
71
|
+
if (!response.ok) throw new Error('Failed to load trace stats');
|
|
72
|
+
const data: TraceStats = await response.json();
|
|
73
|
+
setStats(data);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Error loading trace stats:', err);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const loadCurrentSettings = async () => {
|
|
80
|
+
// TODO: Load current .env settings from backend
|
|
81
|
+
// For now, using defaults
|
|
82
|
+
setAutoTrace(true);
|
|
83
|
+
setTraceFile(true);
|
|
84
|
+
setAutoWorkflow(false);
|
|
85
|
+
setTraceTtlDays(30);
|
|
86
|
+
setSelectedServices(['flock', 'agent', 'dspyengine', 'outpututilitycomponent']);
|
|
87
|
+
setIgnoredOperations(['DashboardEventCollector.set_websocket_manager']);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClearTraces = async () => {
|
|
91
|
+
if (!confirm('Clear all traces from the database? This cannot be undone.')) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setLoading(true);
|
|
96
|
+
setError(null);
|
|
97
|
+
setSuccessMessage(null);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch('/api/traces/clear', { method: 'POST' });
|
|
101
|
+
if (!response.ok) throw new Error('Failed to clear traces');
|
|
102
|
+
|
|
103
|
+
const result = await response.json();
|
|
104
|
+
if (result.success) {
|
|
105
|
+
setSuccessMessage(`Successfully deleted ${result.deleted_count} trace spans`);
|
|
106
|
+
loadTraceStats(); // Refresh stats
|
|
107
|
+
} else {
|
|
108
|
+
setError(result.error || 'Failed to clear traces');
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
// Clear success message after 5 seconds
|
|
115
|
+
setTimeout(() => setSuccessMessage(null), 5000);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleSaveSettings = async () => {
|
|
120
|
+
setLoading(true);
|
|
121
|
+
setError(null);
|
|
122
|
+
setSuccessMessage(null);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// TODO: Implement backend endpoint to save to .env
|
|
126
|
+
// For now, just show success
|
|
127
|
+
setSuccessMessage('Settings saved successfully (persistence coming soon)');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
setError(err instanceof Error ? err.message : 'Failed to save settings');
|
|
130
|
+
} finally {
|
|
131
|
+
setLoading(false);
|
|
132
|
+
setTimeout(() => setSuccessMessage(null), 5000);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const formatDate = (dateStr: string | null) => {
|
|
137
|
+
if (!dateStr) return 'N/A';
|
|
138
|
+
return new Date(dateStr).toLocaleString();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const formatSize = (sizeMb: number) => {
|
|
142
|
+
if (sizeMb < 1) return `${(sizeMb * 1024).toFixed(1)} KB`;
|
|
143
|
+
return `${sizeMb.toFixed(2)} MB`;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
{/* Error/Success Messages */}
|
|
149
|
+
{error && (
|
|
150
|
+
<div className="settings-message settings-message-error" style={{
|
|
151
|
+
padding: 'var(--space-component-sm)',
|
|
152
|
+
marginBottom: 'var(--space-component-md)',
|
|
153
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
154
|
+
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
155
|
+
borderRadius: 'var(--radius-md)',
|
|
156
|
+
color: '#ef4444'
|
|
157
|
+
}}>
|
|
158
|
+
{error}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{successMessage && (
|
|
163
|
+
<div className="settings-message settings-message-success" style={{
|
|
164
|
+
padding: 'var(--space-component-sm)',
|
|
165
|
+
marginBottom: 'var(--space-component-md)',
|
|
166
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
167
|
+
border: '1px solid rgba(16, 185, 129, 0.3)',
|
|
168
|
+
borderRadius: 'var(--radius-md)',
|
|
169
|
+
color: '#10b981'
|
|
170
|
+
}}>
|
|
171
|
+
{successMessage}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* Core Tracing Toggles */}
|
|
176
|
+
<div className="settings-section">
|
|
177
|
+
<h3 className="settings-section-title">OpenTelemetry Tracing</h3>
|
|
178
|
+
|
|
179
|
+
<div className="settings-field">
|
|
180
|
+
<div className="settings-checkbox-wrapper">
|
|
181
|
+
<input
|
|
182
|
+
id="auto-trace"
|
|
183
|
+
type="checkbox"
|
|
184
|
+
checked={autoTrace}
|
|
185
|
+
onChange={(e) => setAutoTrace(e.target.checked)}
|
|
186
|
+
className="settings-checkbox"
|
|
187
|
+
/>
|
|
188
|
+
<label htmlFor="auto-trace" className="settings-checkbox-label">
|
|
189
|
+
Enable auto-tracing (FLOCK_AUTO_TRACE)
|
|
190
|
+
</label>
|
|
191
|
+
</div>
|
|
192
|
+
<p className="settings-description">
|
|
193
|
+
Automatically trace all agent operations and system events with OpenTelemetry
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="settings-field">
|
|
198
|
+
<div className="settings-checkbox-wrapper">
|
|
199
|
+
<input
|
|
200
|
+
id="trace-file"
|
|
201
|
+
type="checkbox"
|
|
202
|
+
checked={traceFile}
|
|
203
|
+
onChange={(e) => setTraceFile(e.target.checked)}
|
|
204
|
+
className="settings-checkbox"
|
|
205
|
+
disabled={!autoTrace}
|
|
206
|
+
/>
|
|
207
|
+
<label htmlFor="trace-file" className="settings-checkbox-label">
|
|
208
|
+
Store traces in DuckDB (FLOCK_TRACE_FILE)
|
|
209
|
+
</label>
|
|
210
|
+
</div>
|
|
211
|
+
<p className="settings-description">
|
|
212
|
+
Save traces to .flock/traces.duckdb for analysis and debugging
|
|
213
|
+
</p>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div className="settings-field">
|
|
217
|
+
<div className="settings-checkbox-wrapper">
|
|
218
|
+
<input
|
|
219
|
+
id="auto-workflow"
|
|
220
|
+
type="checkbox"
|
|
221
|
+
checked={autoWorkflow}
|
|
222
|
+
onChange={(e) => setAutoWorkflow(e.target.checked)}
|
|
223
|
+
className="settings-checkbox"
|
|
224
|
+
disabled={!autoTrace}
|
|
225
|
+
/>
|
|
226
|
+
<label htmlFor="auto-workflow" className="settings-checkbox-label">
|
|
227
|
+
Unified workflow tracing (FLOCK_AUTO_WORKFLOW_TRACE)
|
|
228
|
+
</label>
|
|
229
|
+
</div>
|
|
230
|
+
<p className="settings-description">
|
|
231
|
+
Automatically wrap operations in unified workflow traces (experimental)
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="settings-field">
|
|
236
|
+
<label htmlFor="trace-ttl" className="settings-label">
|
|
237
|
+
Trace Time-To-Live (FLOCK_TRACE_TTL_DAYS)
|
|
238
|
+
</label>
|
|
239
|
+
<p className="settings-description">
|
|
240
|
+
Auto-delete traces older than specified days (leave empty to keep forever)
|
|
241
|
+
</p>
|
|
242
|
+
<input
|
|
243
|
+
id="trace-ttl"
|
|
244
|
+
type="number"
|
|
245
|
+
min="1"
|
|
246
|
+
max="365"
|
|
247
|
+
value={traceTtlDays ?? ''}
|
|
248
|
+
onChange={(e) => setTraceTtlDays(e.target.value ? parseInt(e.target.value) : null)}
|
|
249
|
+
className="settings-input"
|
|
250
|
+
placeholder="30"
|
|
251
|
+
disabled={!autoTrace}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* Service Whitelist */}
|
|
257
|
+
<div className="settings-section">
|
|
258
|
+
<h3 className="settings-section-title">Service Whitelist (FLOCK_TRACE_SERVICES)</h3>
|
|
259
|
+
<p className="settings-description" style={{ marginBottom: 'var(--space-component-md)' }}>
|
|
260
|
+
Only trace specific services. Leave empty to trace all services.
|
|
261
|
+
</p>
|
|
262
|
+
|
|
263
|
+
<MultiSelect
|
|
264
|
+
options={availableServices}
|
|
265
|
+
selected={selectedServices}
|
|
266
|
+
onChange={setSelectedServices}
|
|
267
|
+
placeholder="Select services to trace..."
|
|
268
|
+
disabled={!autoTrace}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Operation Blacklist */}
|
|
273
|
+
<div className="settings-section">
|
|
274
|
+
<h3 className="settings-section-title">Operation Blacklist (FLOCK_TRACE_IGNORE)</h3>
|
|
275
|
+
<p className="settings-description" style={{ marginBottom: 'var(--space-component-md)' }}>
|
|
276
|
+
Exclude specific operations from tracing (format: Service.method)
|
|
277
|
+
</p>
|
|
278
|
+
|
|
279
|
+
<MultiSelect
|
|
280
|
+
options={availableOperations}
|
|
281
|
+
selected={ignoredOperations}
|
|
282
|
+
onChange={setIgnoredOperations}
|
|
283
|
+
placeholder="Select operations to ignore..."
|
|
284
|
+
disabled={!autoTrace}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Database Statistics */}
|
|
289
|
+
<div className="settings-section">
|
|
290
|
+
<h3 className="settings-section-title">Trace Database Statistics</h3>
|
|
291
|
+
|
|
292
|
+
{stats && (
|
|
293
|
+
<div style={{
|
|
294
|
+
backgroundColor: 'rgba(96, 165, 250, 0.05)',
|
|
295
|
+
border: '1px solid rgba(96, 165, 250, 0.2)',
|
|
296
|
+
borderRadius: 'var(--radius-md)',
|
|
297
|
+
padding: 'var(--space-component-md)'
|
|
298
|
+
}}>
|
|
299
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--gap-md)' }}>
|
|
300
|
+
<div>
|
|
301
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
302
|
+
Total Spans
|
|
303
|
+
</div>
|
|
304
|
+
<div style={{ fontSize: 'var(--font-size-lg)', fontWeight: 600 }}>
|
|
305
|
+
{stats.total_spans.toLocaleString()}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div>
|
|
310
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
311
|
+
Total Traces
|
|
312
|
+
</div>
|
|
313
|
+
<div style={{ fontSize: 'var(--font-size-lg)', fontWeight: 600 }}>
|
|
314
|
+
{stats.total_traces.toLocaleString()}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div>
|
|
319
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
320
|
+
Services Traced
|
|
321
|
+
</div>
|
|
322
|
+
<div style={{ fontSize: 'var(--font-size-lg)', fontWeight: 600 }}>
|
|
323
|
+
{stats.services_count}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div>
|
|
328
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
329
|
+
Database Size
|
|
330
|
+
</div>
|
|
331
|
+
<div style={{ fontSize: 'var(--font-size-lg)', fontWeight: 600 }}>
|
|
332
|
+
{formatSize(stats.database_size_mb)}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div>
|
|
337
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
338
|
+
Oldest Trace
|
|
339
|
+
</div>
|
|
340
|
+
<div style={{ fontSize: 'var(--font-size-xs)' }}>
|
|
341
|
+
{formatDate(stats.oldest_trace)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div>
|
|
346
|
+
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-xs)' }}>
|
|
347
|
+
Newest Trace
|
|
348
|
+
</div>
|
|
349
|
+
<div style={{ fontSize: 'var(--font-size-xs)' }}>
|
|
350
|
+
{formatDate(stats.newest_trace)}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
<div style={{ marginTop: 'var(--space-component-md)' }}>
|
|
358
|
+
<button
|
|
359
|
+
onClick={handleClearTraces}
|
|
360
|
+
disabled={loading || !stats || stats.total_spans === 0}
|
|
361
|
+
className="settings-reset-button"
|
|
362
|
+
style={{
|
|
363
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
364
|
+
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
365
|
+
color: '#ef4444'
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{loading ? 'Clearing...' : 'Clear All Traces'}
|
|
369
|
+
</button>
|
|
370
|
+
<p className="settings-description" style={{ marginTop: 'var(--space-xs)' }}>
|
|
371
|
+
Delete all spans and run VACUUM to reclaim disk space
|
|
372
|
+
</p>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{/* Save Settings */}
|
|
377
|
+
<div className="settings-section">
|
|
378
|
+
<button
|
|
379
|
+
onClick={handleSaveSettings}
|
|
380
|
+
disabled={loading}
|
|
381
|
+
className="settings-save-button"
|
|
382
|
+
style={{
|
|
383
|
+
backgroundColor: 'rgba(96, 165, 250, 0.1)',
|
|
384
|
+
border: '1px solid rgba(96, 165, 250, 0.3)',
|
|
385
|
+
color: '#60a5fa',
|
|
386
|
+
padding: 'var(--space-component-sm) var(--space-component-md)',
|
|
387
|
+
borderRadius: 'var(--radius-md)',
|
|
388
|
+
fontSize: 'var(--font-size-base)',
|
|
389
|
+
fontWeight: 500,
|
|
390
|
+
cursor: loading ? 'not-allowed' : 'pointer',
|
|
391
|
+
opacity: loading ? 0.6 : 1
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
{loading ? 'Saving...' : 'Save Tracing Settings'}
|
|
395
|
+
</button>
|
|
396
|
+
<p className="settings-description" style={{ marginTop: 'var(--space-xs)' }}>
|
|
397
|
+
Settings will be persisted to .env file (requires backend restart)
|
|
398
|
+
</p>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export default TracingSettings;
|