Skip to content

Commit 4435018

Browse files
author
Marvin Zhang
committed
feat: Integrate automatic historical data synchronization into start command with --no-history option
1 parent a677a3b commit 4435018

File tree

2 files changed

+220
-16
lines changed

2 files changed

+220
-16
lines changed

cmd/devlog/main.go

Lines changed: 206 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,18 @@ Supports: GitHub Copilot, Claude Code, Cursor, and more.`,
6464
var startCmd = &cobra.Command{
6565
Use: "start",
6666
Short: "Start the collector daemon",
67-
Long: "Start the collector daemon to monitor AI agent logs",
67+
Long: `Start the collector daemon to monitor AI agent logs.
68+
69+
By default, the collector will sync historical data before starting real-time
70+
watching. Use --no-history to skip historical sync (for power users).`,
6871
RunE: func(cmd *cobra.Command, args []string) error {
6972
log.Info("Starting Devlog Collector...")
7073
log.Infof("Version: %s", version)
7174

75+
// Parse flags
76+
skipHistory, _ := cmd.Flags().GetBool("no-history")
77+
initialSyncDays, _ := cmd.Flags().GetInt("initial-sync-days")
78+
7279
// Load configuration
7380
var err error
7481
cfg, err = config.LoadConfig(configPath)
@@ -159,6 +166,70 @@ var startCmd = &cobra.Command{
159166
return fmt.Errorf("failed to discover logs: %w", err)
160167
}
161168

169+
// Create context for graceful shutdown
170+
ctx, cancel := context.WithCancel(context.Background())
171+
defer cancel()
172+
173+
// Sync historical data before starting watcher (unless --no-history)
174+
if !skipHistory {
175+
log.Info("Syncing historical data...")
176+
177+
// Initialize hierarchy cache with client for backfill
178+
hierarchyCacheWithClient := hierarchy.NewHierarchyCache(apiClient, log)
179+
backfillRegistry := adapters.DefaultRegistry(cfg.ProjectID, hierarchyCacheWithClient, log)
180+
181+
// Create backfill manager
182+
backfillConfig := backfill.Config{
183+
Registry: backfillRegistry,
184+
Buffer: buf,
185+
Client: apiClient,
186+
StateDBPath: cfg.Buffer.DBPath,
187+
Logger: log,
188+
}
189+
manager, err := backfill.NewBackfillManager(backfillConfig)
190+
if err != nil {
191+
log.Warnf("Failed to create backfill manager, skipping historical sync: %v", err)
192+
} else {
193+
defer manager.Close()
194+
195+
// Calculate date range for initial sync
196+
fromDate := time.Now().AddDate(0, 0, -initialSyncDays)
197+
toDate := time.Now()
198+
199+
totalSynced := 0
200+
totalSkipped := 0
201+
202+
for agentName, logs := range discovered {
203+
adapterName := mapAgentName(agentName)
204+
205+
for _, logInfo := range logs {
206+
log.Infof("Syncing historical data for %s: %s", agentName, logInfo.Path)
207+
208+
bfConfig := backfill.BackfillConfig{
209+
AgentName: adapterName,
210+
LogPath: logInfo.Path,
211+
FromDate: fromDate,
212+
ToDate: toDate,
213+
BatchSize: 100,
214+
}
215+
216+
result, err := manager.Backfill(ctx, bfConfig)
217+
if err != nil {
218+
log.Warnf("Failed to sync historical data for %s: %v", logInfo.Path, err)
219+
continue
220+
}
221+
222+
totalSynced += result.ProcessedEvents
223+
totalSkipped += result.SkippedEvents
224+
}
225+
}
226+
227+
log.Infof("Historical sync complete: %d events synced, %d skipped (already synced)", totalSynced, totalSkipped)
228+
}
229+
} else {
230+
log.Info("Skipping historical sync (--no-history flag)")
231+
}
232+
162233
for agentName, logs := range discovered {
163234
adapterName := mapAgentName(agentName)
164235
adapterInstance, err := registry.Get(adapterName)
@@ -176,9 +247,6 @@ var startCmd = &cobra.Command{
176247
}
177248

178249
// Process events from watcher to client
179-
ctx, cancel := context.WithCancel(context.Background())
180-
defer cancel()
181-
182250
go func() {
183251
for {
184252
select {
@@ -289,9 +357,19 @@ This is useful for capturing development history before the collector was instal
289357

290358
var backfillRunCmd = &cobra.Command{
291359
Use: "run",
292-
Short: "Run backfill operation",
293-
Long: "Process historical logs for the specified agent and date range",
360+
Short: "Run backfill operation (deprecated)",
361+
Long: `Process historical logs for the specified agent and date range.
362+
363+
DEPRECATED: The 'start' command now automatically syncs historical data.
364+
Use 'devlog-collector start' instead. To skip historical sync, use 'start --no-history'.`,
294365
RunE: func(cmd *cobra.Command, args []string) error {
366+
// Show deprecation warning
367+
fmt.Println("⚠️ DEPRECATED: The 'backfill run' command is deprecated.")
368+
fmt.Println(" The 'start' command now automatically syncs historical data.")
369+
fmt.Println(" Use 'devlog-collector start' for automatic sync,")
370+
fmt.Println(" or 'devlog-collector start --no-history' to skip historical sync.")
371+
fmt.Println()
372+
295373
// Load configuration
296374
var err error
297375
cfg, err = config.LoadConfig(configPath)
@@ -577,6 +655,117 @@ func progressBar(percentage float64) string {
577655
return bar
578656
}
579657

658+
var syncCmd = &cobra.Command{
659+
Use: "sync",
660+
Short: "Manage sync state",
661+
Long: "View and manage the synchronization state of agent logs",
662+
}
663+
664+
var syncStatusCmd = &cobra.Command{
665+
Use: "status",
666+
Short: "Check sync status for all agents",
667+
Long: "Display the synchronization status for all discovered agent logs",
668+
RunE: func(cmd *cobra.Command, args []string) error {
669+
// Load configuration
670+
var err error
671+
cfg, err = config.LoadConfig(configPath)
672+
if err != nil {
673+
return fmt.Errorf("failed to load configuration: %w", err)
674+
}
675+
676+
// Create backfill manager (uses same state store)
677+
backfillConfig := backfill.Config{
678+
StateDBPath: cfg.Buffer.DBPath,
679+
Logger: log,
680+
}
681+
manager, err := backfill.NewBackfillManager(backfillConfig)
682+
if err != nil {
683+
return fmt.Errorf("failed to create sync manager: %w", err)
684+
}
685+
defer manager.Close()
686+
687+
// Get agent filter
688+
agentFilter, _ := cmd.Flags().GetString("agent")
689+
690+
// Discover all agent logs
691+
discovered, err := watcher.DiscoverAllAgentLogs()
692+
if err != nil {
693+
return fmt.Errorf("failed to discover logs: %w", err)
694+
}
695+
696+
fmt.Println("📊 Sync Status")
697+
fmt.Println("==============")
698+
fmt.Println()
699+
700+
totalSynced := 0
701+
totalPending := 0
702+
totalInProgress := 0
703+
704+
for agentName, logs := range discovered {
705+
adapterName := mapAgentName(agentName)
706+
707+
// Filter by agent if specified
708+
if agentFilter != "" && agentFilter != agentName && agentFilter != adapterName {
709+
continue
710+
}
711+
712+
fmt.Printf("🤖 %s (%d log sources)\n", agentName, len(logs))
713+
714+
states, err := manager.Status(adapterName)
715+
if err != nil {
716+
fmt.Printf(" ❌ Error getting status: %v\n", err)
717+
continue
718+
}
719+
720+
// Create a map for quick lookup
721+
stateMap := make(map[string]*backfill.BackfillState)
722+
for _, state := range states {
723+
stateMap[state.LogFilePath] = state
724+
}
725+
726+
for _, logInfo := range logs {
727+
state, exists := stateMap[logInfo.Path]
728+
if !exists {
729+
fmt.Printf(" 📁 %s\n", logInfo.Path)
730+
fmt.Printf(" Status: ⏳ pending (not yet synced)\n")
731+
totalPending++
732+
continue
733+
}
734+
735+
fmt.Printf(" 📁 %s\n", logInfo.Path)
736+
switch state.Status {
737+
case backfill.StatusCompleted:
738+
fmt.Printf(" Status: ✅ synced (%d events)\n", state.TotalEventsProcessed)
739+
if state.CompletedAt != nil {
740+
fmt.Printf(" Last sync: %s\n", state.CompletedAt.Format(time.RFC3339))
741+
}
742+
totalSynced++
743+
case backfill.StatusInProgress:
744+
fmt.Printf(" Status: 🔄 syncing (%d events so far)\n", state.TotalEventsProcessed)
745+
totalInProgress++
746+
case backfill.StatusPaused:
747+
fmt.Printf(" Status: ⏸️ paused (%d events)\n", state.TotalEventsProcessed)
748+
totalPending++
749+
case backfill.StatusFailed:
750+
fmt.Printf(" Status: ❌ failed: %s\n", state.ErrorMessage)
751+
totalPending++
752+
default:
753+
fmt.Printf(" Status: ⏳ pending\n")
754+
totalPending++
755+
}
756+
}
757+
fmt.Println()
758+
}
759+
760+
fmt.Println("Summary:")
761+
fmt.Printf(" ✅ Synced: %d\n", totalSynced)
762+
fmt.Printf(" 🔄 In progress: %d\n", totalInProgress)
763+
fmt.Printf(" ⏳ Pending: %d\n", totalPending)
764+
765+
return nil
766+
},
767+
}
768+
580769
func init() {
581770
// Configure logging
582771
log.SetFormatter(&logrus.TextFormatter{
@@ -589,11 +778,19 @@ func init() {
589778
rootCmd.AddCommand(versionCmd)
590779
rootCmd.AddCommand(statusCmd)
591780
rootCmd.AddCommand(backfillCmd)
781+
rootCmd.AddCommand(syncCmd)
592782

593783
// Add backfill subcommands
594784
backfillCmd.AddCommand(backfillRunCmd)
595785
backfillCmd.AddCommand(backfillStatusCmd)
596786

787+
// Add sync subcommands
788+
syncCmd.AddCommand(syncStatusCmd)
789+
790+
// Start command flags
791+
startCmd.Flags().Bool("no-history", false, "Skip historical sync (only watch for new events)")
792+
startCmd.Flags().Int("initial-sync-days", 90, "Number of days to sync on first run")
793+
597794
// Backfill run flags
598795
backfillRunCmd.Flags().StringP("agent", "a", "copilot", "Agent name (copilot, claude, cursor)")
599796
backfillRunCmd.Flags().StringP("from", "f", "", "Start date (YYYY-MM-DD)")
@@ -606,6 +803,9 @@ func init() {
606803
// Backfill status flags
607804
backfillStatusCmd.Flags().StringP("agent", "a", "", "Agent name to check")
608805

806+
// Sync status flags
807+
syncStatusCmd.Flags().StringP("agent", "a", "", "Filter by agent name")
808+
609809
// Global flags
610810
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
611811
"~/.devlog/collector.json", "Path to configuration file")

specs/016-automatic-historical-sync/README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
status: planned
2+
status: in-progress
33
created: '2025-12-05'
44
tags:
55
- ux
@@ -8,11 +8,15 @@ tags:
88
- sync
99
priority: high
1010
created_at: '2025-12-05T04:55:09.494Z'
11+
updated_at: '2025-12-05T06:32:07.677Z'
12+
transitions:
13+
- status: in-progress
14+
at: '2025-12-05T06:32:07.677Z'
1115
---
1216

1317
# Automatic Historical Data Synchronization
1418

15-
> **Status**: 📅 Planned · **Priority**: High · **Created**: 2025-12-05
19+
> **Status**: ⏳ In progress · **Priority**: High · **Created**: 2025-12-05 · **Tags**: ux, workflow, collector, sync
1620
1721
## Problem Statement
1822

@@ -118,27 +122,27 @@ devlog-collector backfill run # → Prints: "Deprecated. Use 'start' instead."
118122

119123
### Phase 1: Merge Backfill into Start Command
120124

121-
- [ ] Add sync cursor management to BackfillManager
122-
- [ ] Integrate historical sync into `start` command flow
123-
- [ ] Process history before starting watcher (or in parallel)
124-
- [ ] Add `--no-history` flag for power users
125+
- [x] Add sync cursor management to BackfillManager
126+
- [x] Integrate historical sync into `start` command flow
127+
- [x] Process history before starting watcher (or in parallel)
128+
- [x] Add `--no-history` flag for power users
125129

126130
### Phase 2: Continuous Cursor Tracking
127131

128-
- [ ] Update cursor after each event batch (watcher and initial sync)
132+
- [x] Update cursor after each event batch (watcher and initial sync)
129133
- [ ] Handle file rotation (new chat sessions)
130134
- [ ] Detect new workspace directories dynamically
131135

132136
### Phase 3: Progress & Status UX
133137

134-
- [ ] Add `sync --status` subcommand
138+
- [x] Add `sync --status` subcommand
135139
- [ ] Show sync progress on startup (non-blocking spinner/bar)
136140
- [ ] Add sync state to `status` command output
137141

138142
### Phase 4: Cleanup & Documentation
139143

140-
- [ ] Deprecate `backfill` command (show migration message)
141-
- [ ] Update README and help text
144+
- [x] Deprecate `backfill` command (show migration message)
145+
- [x] Update README and help text
142146
- [ ] Test full user journey from fresh install
143147

144148
## Test

0 commit comments

Comments
 (0)