pico
created pr with
103.1
added 103.2
1: 66cf9b9 = 1: 66cf9b9 feat(pipe): add pipe_monitors table
2: 83b0c70 = 2: 83b0c70 chore(pipe): add new db methods
3: 43aa4a1 = 3: 43aa4a1 chore(pipe): add db impl
4: a75186b = 4: a75186b chore(pipe): add tests for monitoring
5: 9c7fcda = 5: 9c7fcda feat(pipe): add monitor cli
6: 2b86dec = 6: 2b86dec feat(pipe): status and rss commands
7: 8df2aa2 = 7: 8df2aa2 feat(pipe): monitor pub and pipe cmd
8: 11a5bce = 8: 11a5bce chore(pipe): monitor help text
9: 670350c = 9: 670350c refactor(pipe): monitor pipes on throttle interval
10: ade2b83 = 10: ade2b83 fix: window and ping fixes
11: 68f5cd1 = 11: 68f5cd1 chore(pipe): add tests
-: ------- > 12: 74fb40f chore(pipe): add logging stmts
-: ------- > 13: c422818 refactor(pipe): create pipe monitors history table
-: ------- > 14: e0c3a88 fix(pipe): only update monitor if within window
-: ------- > 15: 9a8fbde chore(pipe): implement monitor history db interface
-: ------- > 16: f168507 feat(pipe): record historical monitors
-: ------- > 17: 16df77f feat(pipe): monitor calculate uptime
-: ------- > 18: 10ffdf1 feat(pipe): uptime cli command
-: ------- > 19: 8231ac5 chore(pipe): monitor history stubs
-: ------- > 20: 65a63a9 fix(pipe): uptime cmd
added 103.3
3: 43aa4a1 ! 1: 603ca6e chore(pubsub): add more tests
1: 66cf9b9 < -: ------- feat(pipe): add pipe_monitors table
2: 83b0c70 < -: ------- chore(pipe): add new db methods
4: a75186b ! 2: 17e00b2 feat(pubsub): round robin
13: c422818 ! 3: e3136bd fix(pubsub): check for eof before processing and skip empty byte reads
-: ------- > 4: 9a6d19e fix: rr
5: 9c7fcda < -: ------- feat(pipe): add monitor cli
16: f168507 ! 5: 5b3f3a1 fix: sending 0 byte read
6: 2b86dec < -: ------- feat(pipe): status and rss commands
11: 68f5cd1 ! 6: 4fff471 refactor: fixes
7: 8df2aa2 < -: ------- feat(pipe): monitor pub and pipe cmd
14: e0c3a88 ! 7: d0dfc85 chore: SetDispatch on Broker
8: 11a5bce < -: ------- chore(pipe): monitor help text
9: 670350c < -: ------- refactor(pipe): monitor pipes on throttle interval
10: ade2b83 < -: ------- fix: window and ping fixes
12: 74fb40f < -: ------- chore(pipe): add logging stmts
15: 9a8fbde < -: ------- chore(pipe): implement monitor history db interface
17: 16df77f < -: ------- feat(pipe): monitor calculate uptime
18: 10ffdf1 < -: ------- feat(pipe): uptime cli command
19: 8231ac5 < -: ------- chore(pipe): monitor history stubs
20: 65a63a9 < -: ------- fix(pipe): uptime cmd
cmds
checkout latest patchset:
ssh pr.pico.sh print 103 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 103.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 103
Patchset
103.2
feat(pipe): add pipe_monitors table
Eric Bower
chore(pipe): add new db methods
2025-12-26T21:02:39ZEric Bower
chore(pipe): add db impl
2025-12-26T21:24:18ZEric Bower
chore(pipe): add tests for monitoring
2025-12-26T21:37:49ZEric Bower
feat(pipe): add monitor cli
2025-12-28T14:41:40ZEric Bower
feat(pipe): status and rss commands
2025-12-28T14:44:04ZEric Bower
feat(pipe): monitor pub and pipe cmd
2025-12-28T14:55:12ZEric Bower
chore(pipe): monitor help text
2025-12-28T15:26:40ZEric Bower
refactor(pipe): monitor pipes on throttle interval
2025-12-28T15:40:57ZEric Bower
fix: window and ping fixes
2025-12-28T16:22:23ZEric Bower
chore(pipe): add tests
2026-01-03T02:26:40ZEric Bower
chore(pipe): add logging stmts
2026-01-03T03:50:20ZEric Bower
refactor(pipe): create pipe monitors history table
2026-01-05T14:46:42ZEric Bower
fix(pipe): only update monitor if within window
2026-01-07T20:00:47ZEric Bower
chore(pipe): implement monitor history db interface
2026-01-07T21:52:08ZEric Bower
feat(pipe): record historical monitors
2026-01-07T23:19:05ZEric Bower
feat(pipe): monitor calculate uptime
2026-01-07T23:27:20ZEric Bower
→ feat(pipe): uptime cli command
2026-01-07T23:27:20ZEric Bower
chore(pipe): monitor history stubs
2026-01-08T00:48:50ZEric Bower
fix(pipe): uptime cmd
2026-01-08T00:52:48ZEric Bower
2026-01-08T01:18:38Z
feat(pipe): uptime cli command
Eric Bower
2026-01-08T00:48:50ZSemantic diff summary
2 added,
1 modified,
0 signature changed,
0 removed
across 1 analyzed file
+93
-0
pkg/apps/pipe/cli.go
#
| ... | ... | @@ -90,6 +90,13 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware { | |
| 90 | 90 | sesh.Fatal(err) | |
| 91 | 91 | } | |
| 92 | 92 | return next(sesh) | |
| 93 | + | case "uptime": | |
| 94 | + | err := handler.uptime(cliCmd, user) | |
| 95 | + | if err != nil { | |
| 96 | + | logger.Error("uptime cmd", "err", err) | |
| 97 | + | sesh.Fatal(err) | |
| 98 | + | } | |
| 99 | + | return next(sesh) | |
| 93 | 100 | case "rss": | |
| 94 | 101 | err := handler.rss(cliCmd, user) | |
| 95 | 102 | if err != nil { |
| ... | ... | @@ -442,6 +449,92 @@ func (handler *CliHandler) status(cmd *CliCmd, user *db.User) error { | |
| 442 | 449 | return nil | |
| 443 | 450 | } | |
| 444 | 451 | ||
| 452 | + | func (handler *CliHandler) uptime(cmd *CliCmd, user *db.User) error { | |
| 453 | + | if user == nil { | |
| 454 | + | return fmt.Errorf("access denied") | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | fs := flag.NewFlagSet("uptime", flag.ContinueOnError) | |
| 458 | + | fs.SetOutput(cmd.sesh) | |
| 459 | + | fromStr := fs.String("from", "", "start time (RFC3339 or duration like '24h', '7d')") | |
| 460 | + | toStr := fs.String("to", "", "end time (RFC3339, defaults to now)") | |
| 461 | + | ||
| 462 | + | if err := fs.Parse(cmd.args); err != nil { | |
| 463 | + | return nil | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | args := fs.Args() | |
| 467 | + | if len(args) == 0 { | |
| 468 | + | _, _ = fmt.Fprintln(cmd.sesh, "usage: uptime <topic> [--from <time>] [--to <time>]") | |
| 469 | + | _, _ = fmt.Fprintln(cmd.sesh, " --from: start time (RFC3339 or duration like '24h', '7d', default: 24h)") | |
| 470 | + | _, _ = fmt.Fprintln(cmd.sesh, " --to: end time (RFC3339, default: now)") | |
| 471 | + | return nil | |
| 472 | + | } | |
| 473 | + | ||
| 474 | + | topic := args[0] | |
| 475 | + | ||
| 476 | + | monitor, err := handler.DBPool.FindPipeMonitorByTopic(user.ID, topic) | |
| 477 | + | if err != nil { | |
| 478 | + | return fmt.Errorf("failed to find monitor: %w", err) | |
| 479 | + | } | |
| 480 | + | if monitor == nil { | |
| 481 | + | return fmt.Errorf("monitor not found: %s", topic) | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | now := time.Now().UTC() | |
| 485 | + | to := now | |
| 486 | + | from := now.Add(-24 * time.Hour) | |
| 487 | + | ||
| 488 | + | if *fromStr != "" { | |
| 489 | + | if parsed, err := time.Parse(time.RFC3339, *fromStr); err == nil { | |
| 490 | + | from = parsed.UTC() | |
| 491 | + | } else if dur, err := parseDuration(*fromStr); err == nil { | |
| 492 | + | from = now.Add(-dur) | |
| 493 | + | } else { | |
| 494 | + | return fmt.Errorf("invalid --from value: %s", *fromStr) | |
| 495 | + | } | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | if *toStr != "" { | |
| 499 | + | if parsed, err := time.Parse(time.RFC3339, *toStr); err == nil { | |
| 500 | + | to = parsed.UTC() | |
| 501 | + | } else { | |
| 502 | + | return fmt.Errorf("invalid --to value: %s", *toStr) | |
| 503 | + | } | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | history, err := handler.DBPool.FindPipeMonitorHistory(monitor.ID, from, to) | |
| 507 | + | if err != nil { | |
| 508 | + | return fmt.Errorf("failed to fetch history: %w", err) | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | result := db.ComputeUptime(history, from, to) | |
| 512 | + | ||
| 513 | + | _, _ = fmt.Fprintf(cmd.sesh, "Monitor: %s\r\n", topic) | |
| 514 | + | _, _ = fmt.Fprintf(cmd.sesh, "Period: %s to %s\r\n", from.Format(time.RFC3339), to.Format(time.RFC3339)) | |
| 515 | + | _, _ = fmt.Fprintf(cmd.sesh, "Total Duration: %s\r\n", result.TotalDuration.Round(time.Second)) | |
| 516 | + | _, _ = fmt.Fprintf(cmd.sesh, "Uptime Duration: %s\r\n", result.UptimeDuration.Round(time.Second)) | |
| 517 | + | _, _ = fmt.Fprintf(cmd.sesh, "Uptime: %.2f%%\r\n", result.UptimePercent) | |
| 518 | + | ||
| 519 | + | return nil | |
| 520 | + | } | |
| 521 | + | ||
| 522 | + | func parseDuration(s string) (time.Duration, error) { | |
| 523 | + | if len(s) == 0 { | |
| 524 | + | return 0, fmt.Errorf("empty duration") | |
| 525 | + | } | |
| 526 | + | last := s[len(s)-1] | |
| 527 | + | if last == 'd' { | |
| 528 | + | var n int | |
| 529 | + | _, err := fmt.Sscanf(s, "%d", &n) | |
| 530 | + | if err != nil { | |
| 531 | + | return 0, fmt.Errorf("invalid duration: %s", s) | |
| 532 | + | } | |
| 533 | + | return time.Duration(n) * 24 * time.Hour, nil | |
| 534 | + | } | |
| 535 | + | return time.ParseDuration(s) | |
| 536 | + | } | |
| 537 | + | ||
| 445 | 538 | func (handler *CliHandler) rss(cmd *CliCmd, user *db.User) error { | |
| 446 | 539 | if user == nil { | |
| 447 | 540 | return fmt.Errorf("access denied") |