Skip to content

Commit 801c3c3

Browse files
authored
Add self monitoring (#8)
1 parent e66b9d5 commit 801c3c3

File tree

3 files changed

+77
-56
lines changed

3 files changed

+77
-56
lines changed

README.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A high-performance load generator for testing log ingestion pipelines, observabi
1212
- **Continuous mode**: Maximum speed testing with `--period 0`
1313
- **Real-time stats**: Current and average throughput metrics
1414
- **Backpressure detection**: Tracks 429/503 responses
15-
- **Process monitoring**: Monitor CPU, memory, and threads of any process
15+
- **Process monitoring**: Monitor CPU, memory, and threads of target processes and loadgen itself
1616
- **Cross-platform**: Supports macOS and Linux
1717
- **Zero dependencies**: Only requires `gofakeit` for data generation
1818

@@ -121,6 +121,9 @@ Process Monitoring Options:
121121
-monitor-pid int
122122
PID of process to monitor (0 means disabled)
123123
124+
-monitor-self
125+
Monitor loadgen's own resource usage (default false)
126+
124127
-monitor-interval duration
125128
Interval for process monitoring stats (default 5s)
126129
```
@@ -248,7 +251,7 @@ Output (JSON array):
248251

249252
### Process Monitoring
250253

251-
Monitor a process without generating load:
254+
Monitor a target process without generating load:
252255

253256
```bash
254257
# Monitor by process name
@@ -263,25 +266,24 @@ Monitor a process without generating load:
263266

264267
**Output:**
265268
```
266-
[MONITOR] pid: 12345 | cpu: 45.3% | memory: 234.5MB | threads: 28
269+
[MONITOR - TARGET] edgedelta | pid: 12345 | cpu: 45.3% | memory: 234.5MB | threads: 28
267270
```
268271

269-
Monitor a process while generating load:
272+
Monitor loadgen's own resource usage:
270273

271274
```bash
272-
# Observe how load impacts the monitored process
273-
./loadgen \
274-
--endpoint http://localhost:8085 \
275-
--format nginx_log \
276-
--workers 48 \
277-
--period 10ms \
278-
--monitor-process edgedelta
275+
# Monitor self only
276+
./loadgen --monitor-self --endpoint http://localhost:8085 --workers 100
277+
278+
# Monitor both loadgen and target process
279+
./loadgen --monitor-self --monitor-process edgedelta --endpoint http://localhost:8085 --workers 100
279280
```
280281

281-
**Output:**
282+
**Output (dual monitoring):**
282283
```
283284
[STATS] current: 1000 logs/sec, 0.64 MB/s | avg: 1000 logs/sec | total: 5000 | errors: 0 | backpressure: 0 (0.0%)
284-
[MONITOR] pid: 12345 | cpu: 45.3% | memory: 234.5MB | threads: 28
285+
[MONITOR - SELF] loadgen | pid: 8045 | cpu: 15.2% | memory: 45.3MB | threads: 11
286+
[MONITOR - TARGET] edgedelta | pid: 12345 | cpu: 45.3% | memory: 234.5MB | threads: 28
285287
```
286288

287289
**Monitoring Metrics:**
@@ -316,6 +318,9 @@ The tool prints real-time statistics every 5 seconds:
316318
- Smaller pools (10-50): Less memory, faster startup, sufficient for most load tests
317319
- Larger pools (500-1000): More variety, useful for testing unique value handling
318320
6. **Format style**: NDJSON (`--format-style ndjson`) is default and works with most log systems
321+
7. **Self-monitoring**: Use `--monitor-self` to ensure loadgen isn't the bottleneck
322+
- If loadgen CPU is maxed out, reduce workers or use multiple instances
323+
- Helps distinguish between generator and target system performance issues
319324

320325
## How It Works
321326

main.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Config struct {
3333
Workers int
3434
MonitorPID int
3535
MonitorProcess string
36+
MonitorSelf bool
3637
MonitorInterval time.Duration
3738
PayloadPoolSize int
3839
}
@@ -188,40 +189,68 @@ func main() {
188189
}()
189190
}
190191

191-
// Determine monitoring PID if monitoring is enabled.
192-
var processStats *ProcessStats
192+
// Determine monitoring targets.
193+
var targetStats *ProcessStats
194+
var selfStats *ProcessStats
195+
193196
if config.MonitorProcess != "" {
194197
pid, err := findProcessByName(config.MonitorProcess)
195198
if err != nil {
196199
log.Fatalf("Failed to find process '%s': %v", config.MonitorProcess, err)
197200
}
198-
processStats = &ProcessStats{
201+
targetStats = &ProcessStats{
199202
pid: pid,
200203
processName: config.MonitorProcess,
204+
monitorType: MonitorTypeTarget,
201205
}
202206
log.Printf("Found process '%s' with PID: %d", config.MonitorProcess, pid)
203207
} else if config.MonitorPID > 0 {
204-
processStats = &ProcessStats{
205-
pid: config.MonitorPID,
208+
targetStats = &ProcessStats{
209+
pid: config.MonitorPID,
210+
monitorType: MonitorTypeTarget,
206211
}
207212
log.Printf("Monitoring process PID: %d", config.MonitorPID)
208213
}
209214

215+
if config.MonitorSelf {
216+
selfStats = &ProcessStats{
217+
pid: os.Getpid(),
218+
processName: "loadgen",
219+
monitorType: MonitorTypeSelf,
220+
}
221+
log.Printf("Monitoring self PID: %d", selfStats.pid)
222+
}
223+
210224
// Determine mode: monitoring-only vs concurrent vs normal.
211-
monitoringEnabled := processStats != nil
225+
monitoringEnabled := targetStats != nil || selfStats != nil
212226
endpointProvided := flag.Lookup("endpoint").Value.String() != flag.Lookup("endpoint").DefValue
213227

214228
if monitoringEnabled && !endpointProvided {
215229
// Mode 2: Standalone monitoring only.
216230
log.Printf("Starting monitoring-only mode (interval: %s)", config.MonitorInterval)
217-
runMonitoringOnly(ctx, processStats, config.MonitorInterval)
231+
232+
if targetStats != nil {
233+
go monitorProcess(ctx, targetStats, config.MonitorInterval)
234+
}
235+
if selfStats != nil {
236+
go monitorProcess(ctx, selfStats, config.MonitorInterval)
237+
}
238+
239+
// Block until context is cancelled.
240+
<-ctx.Done()
241+
log.Println("Monitoring stopped")
218242
return
219243
}
220244

221245
if monitoringEnabled {
222246
// Mode 1: Concurrent monitoring + load generation.
223247
log.Printf("Starting concurrent monitoring (interval: %s)", config.MonitorInterval)
224-
go monitorProcess(ctx, processStats, config.MonitorInterval)
248+
if targetStats != nil {
249+
go monitorProcess(ctx, targetStats, config.MonitorInterval)
250+
}
251+
if selfStats != nil {
252+
go monitorProcess(ctx, selfStats, config.MonitorInterval)
253+
}
225254
}
226255

227256
// Normal load generation (with or without monitoring).
@@ -245,6 +274,7 @@ func parseFlags() *Config {
245274
workers := flag.Int("workers", 1, "Number of concurrent workers")
246275
monitorPID := flag.Int("monitor-pid", 0, "PID of process to monitor (0 means disabled)")
247276
monitorProcess := flag.String("monitor-process", "", "Process name to monitor (e.g., 'edgedelta')")
277+
monitorSelf := flag.Bool("monitor-self", false, "Monitor loadgen's own resource usage")
248278
monitorInterval := flag.Duration("monitor-interval", 5*time.Second, "Interval for process monitoring stats")
249279
payloadPoolSize := flag.Int("payload-pool-size", 100, "Number of unique payloads to generate in the pool")
250280
flag.Parse()
@@ -261,6 +291,7 @@ func parseFlags() *Config {
261291
Workers: *workers,
262292
MonitorPID: *monitorPID,
263293
MonitorProcess: *monitorProcess,
294+
MonitorSelf: *monitorSelf,
264295
MonitorInterval: *monitorInterval,
265296
PayloadPoolSize: *payloadPoolSize,
266297
}

monitor.go

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import (
1313
"time"
1414
)
1515

16+
// MonitorType represents the type of process monitoring.
17+
type MonitorType string
18+
19+
const (
20+
// MonitorTypeSelf indicates monitoring of loadgen's own process.
21+
MonitorTypeSelf MonitorType = "SELF"
22+
// MonitorTypeTarget indicates monitoring of a target process.
23+
MonitorTypeTarget MonitorType = "TARGET"
24+
)
25+
1626
// ProcessStats tracks process monitoring metrics.
1727
type ProcessStats struct {
1828
mu sync.Mutex
@@ -21,6 +31,7 @@ type ProcessStats struct {
2131
memoryMB float64
2232
threadCount int
2333
processName string
34+
monitorType MonitorType
2435
}
2536

2637
func (ps *ProcessStats) update(cpu float64, mem float64, threads int) {
@@ -34,8 +45,15 @@ func (ps *ProcessStats) update(cpu float64, mem float64, threads int) {
3445
func (ps *ProcessStats) print() {
3546
ps.mu.Lock()
3647
defer ps.mu.Unlock()
37-
fmt.Printf("[MONITOR] pid: %d | cpu: %.1f%% | memory: %.1fMB | threads: %d\n",
38-
ps.pid, ps.cpuPercent, ps.memoryMB, ps.threadCount)
48+
49+
prefix := fmt.Sprintf("[MONITOR - %s]", ps.monitorType)
50+
if ps.processName != "" {
51+
fmt.Printf("%s %s | pid: %d | cpu: %.1f%% | memory: %.1fMB | threads: %d\n",
52+
prefix, ps.processName, ps.pid, ps.cpuPercent, ps.memoryMB, ps.threadCount)
53+
} else {
54+
fmt.Printf("%s pid: %d | cpu: %.1f%% | memory: %.1fMB | threads: %d\n",
55+
prefix, ps.pid, ps.cpuPercent, ps.memoryMB, ps.threadCount)
56+
}
3957
}
4058

4159
// findProcessByName finds a process PID by name using pgrep.
@@ -264,36 +282,3 @@ func monitorProcess(ctx context.Context, stats *ProcessStats, interval time.Dura
264282
}
265283
}
266284
}
267-
268-
// runMonitoringOnly runs monitoring as the main execution path (blocking).
269-
func runMonitoringOnly(ctx context.Context, stats *ProcessStats, interval time.Duration) {
270-
log.Printf("Monitoring process PID %d (press Ctrl+C to stop)", stats.pid)
271-
272-
ticker := time.NewTicker(interval)
273-
defer ticker.Stop()
274-
275-
sampleInterval := calculateSampleInterval(interval)
276-
277-
cpu, mem, threads, err := collectProcessMetrics(stats.pid, sampleInterval)
278-
if err != nil {
279-
log.Fatalf("Failed to collect initial metrics: %v", err)
280-
}
281-
stats.update(cpu, mem, threads)
282-
stats.print()
283-
284-
for {
285-
select {
286-
case <-ctx.Done():
287-
log.Println("Monitoring stopped")
288-
return
289-
case <-ticker.C:
290-
cpu, mem, threads, err := collectProcessMetrics(stats.pid, sampleInterval)
291-
if err != nil {
292-
log.Printf("Process terminated or monitoring failed: %v", err)
293-
return
294-
}
295-
stats.update(cpu, mem, threads)
296-
stats.print()
297-
}
298-
}
299-
}

0 commit comments

Comments
 (0)