Skip to content

feat(metrics): Collect event-driven Automation metrics#929

Open
boxhock wants to merge 14 commits intoautobrr:developfrom
boxhock:feat/automations-metrics
Open

feat(metrics): Collect event-driven Automation metrics#929
boxhock wants to merge 14 commits intoautobrr:developfrom
boxhock:feat/automations-metrics

Conversation

@boxhock
Copy link
Copy Markdown

@boxhock boxhock commented Jan 1, 2026

It doesn't seem like we're currently collecting event-driven metrics in this project, so I took some liberties in how to structurethat. I'm open to refactoring.

This will expose the following new metrics:

# Total number of automation rule runs
qui_automation_rule_run_total{instance_id, instance_name, rule_id, rule_name}

# Total number of torrents that matched the trackers in the rule
qui_automation_rule_run_torrents_matched_total{instance_id, instance_name, rule_id, rule_name}

# Total number of automation rule actions
qui_automation_rule_run_action_total{instance_id, instance_name, rule_id, rule_name, action}

# Total number of automation rule actions where conditions were met but the action was not performed
qui_automation_rule_run_action_not_performed_total{instance_id, instance_name, rule_id, rule_name, reason}

Summary by CodeRabbit

  • New Features

    • Added automation metrics: per-rule run counts, per-action outcomes, and split category metrics ("no match" vs "blocked"); metrics are wired into the metrics system and exposed when enabled.
  • Tests

    • Added tests validating automation metrics emission and the automation metrics accessor.
  • Chores

    • Service now accepts and uses an automation metrics collector to emit per-rule metrics after processing.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 1, 2026

Walkthrough

Wires a new Prometheus AutomationCollector into MetricsManager, exposes it via GetAutomationCollector, injects it into the automations Service with SetMetricsCollector, and emits per-rule/action counters during rule processing. No public API signature changes. (≈30 words)

Changes

Cohort / File(s) Summary
Metrics collector
internal/metrics/collector/automation.go
New AutomationCollector type with four CounterVec metrics, NewAutomationCollector constructor, label helpers, and Prometheus registration.
Metrics manager
internal/metrics/..., internal/metrics/metrics.go, internal/metrics/manager_test.go
MetricsManager gains automationCollector *collector.AutomationCollector, initializes and registers it in NewMetricsManager, and exposes GetAutomationCollector(); tests updated to assert registration and Describe behaviour.
Automation service wiring
internal/services/automations/service.go
Service gains metricsCollector *collector.AutomationCollector, SetMetricsCollector(*collector.AutomationCollector) setter, collectRuleRunMetrics(...), and defers metrics emission after per-instance processing when collector is present.
Rule processing metrics
internal/services/automations/processor.go, internal/services/automations/processor_test.go
ruleRunStats.CollectMetrics(...) added to increment rule run totals, matched torrent counts, per-action and not-performed counters; category metric split into categoryNoMatch and categoryBlocked; tests added to validate metrics emission.
CLI/main wiring
cmd/qui/main.go
After creating MetricsManager, main sets the automation service's metrics collector via MetricsManager.GetAutomationCollector() before starting the metrics server.

Sequence Diagram(s)

sequenceDiagram
    participant Main
    participant MetricsManager
    participant AutomationCollector as Collector
    participant AutomationService as Service
    participant Processor

    Main->>MetricsManager: NewMetricsManager()
    Note right of MetricsManager: constructs & registers collectors\n(including AutomationCollector)
    MetricsManager->>Collector: NewAutomationCollector() & MustRegister
    Collector-->>MetricsManager: registered collector

    Main->>Service: SetMetricsCollector(automationCollector)
    Service-->>Service: store collector

    Note over Processor,Service: During per-instance rule processing
    Processor->>Service: process rules -> produce ruleRunStats
    Service->>Processor: processing completes
    Processor->>Collector: CollectMetrics(rule, stats, instanceName)
    Collector-->>Collector: increment counters (rule runs, matches,\nactions, not-performed)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement, automations

Suggested reviewers

  • s0up4200
  • Audionut

Poem

🐰
I hopped where counters gently grow,
Each rule I met, I helped to know,
Actions tallied, totals bright,
Metrics hummed beneath moonlight,
A rabbit cheers for observability delight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding event-driven automation metrics collection to the system.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8054c2e and 5ccd01e.

📒 Files selected for processing (2)
  • cmd/qui/main.go
  • internal/services/automations/processor.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/qui/main.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/processor.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/processor.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/services/automations/processor.go
🧬 Code graph analysis (1)
internal/services/automations/processor.go (1)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-14)
🔇 Additional comments (4)
internal/services/automations/processor.go (4)

11-11: LGTM!

The import addition is appropriate for the metrics collection functionality.


61-62: LGTM!

The split from CategoryConditionNotMetOrBlocked into two distinct counters improves clarity and aligns with the discussion in past reviews. This separation enables better observability by distinguishing between conditions not being met versus actions being blocked by cross-seed protection.


67-82: LGTM!

The CollectMetrics method is well-implemented and correctly emits metrics for:

  • Overall rule runs and torrents matched
  • Per-action outcomes (speed_limit, share_limit, pause, tag, category, delete)
  • Specific cases where actions couldn't be performed (tag_skipped_missing_unregistered_set, category_blocked)

The implementation appropriately follows the discussion from past reviews by focusing on actionable metrics (state changes and blocked actions) rather than condition-not-met counters that would increment on every evaluation.


278-282: LGTM!

The category metric increment logic is now correct. When the category change is not applied:

  • If shouldApply is true, the action was blocked by cross-seed protection → correctly increments CategoryBlocked
  • If shouldApply is false, the condition wasn't met → correctly increments CategoryConditionNotMet

This properly addresses the inverted logic issue from the previous review.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/services/automations/service.go (1)

99-101: Consider thread safety for SetMetricsCollector.

The setter modifies s.metricsCollector without synchronization. Since this is called during startup (in main.go) before the service loop processes rules, it's safe in the current flow. However, if this setter is ever called while the service is running, there could be a data race with collectRuleRunMetrics reading the field.

For future-proofing, consider using sync.Once or a mutex, or documenting that this must be called before Start().

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9586084 and 765c7d7.

📒 Files selected for processing (7)
  • cmd/qui/main.go
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
  • internal/metrics/metrics.go
  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/service.go
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/metrics/metrics.go
  • internal/services/automations/service.go
  • internal/services/automations/processor_test.go
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
  • internal/services/automations/processor.go
  • cmd/qui/main.go
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/service.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/service.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-11-25T22:46:03.762Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.

Applied to files:

  • internal/services/automations/processor_test.go
🧬 Code graph analysis (5)
internal/metrics/metrics.go (1)
internal/metrics/collector/automation.go (2)
  • AutomationCollector (9-26)
  • NewAutomationCollector (34-119)
internal/services/automations/service.go (2)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-26)
internal/models/automation.go (1)
  • Automation (34-46)
internal/services/automations/processor_test.go (2)
internal/models/automation.go (3)
  • Automation (34-46)
  • ActionConditions (448-456)
  • CategoryAction (498-506)
internal/metrics/collector/automation.go (2)
  • NewAutomationCollector (34-119)
  • GetAutomationRuleRunLabels (30-32)
internal/metrics/manager_test.go (2)
internal/metrics/metrics.go (1)
  • NewMetricsManager (22-42)
internal/metrics/collector/automation.go (2)
  • AutomationCollector (9-26)
  • NewAutomationCollector (34-119)
internal/services/automations/processor.go (1)
internal/metrics/collector/automation.go (2)
  • AutomationCollector (9-26)
  • GetAutomationRuleRunLabels (30-32)
🔇 Additional comments (12)
internal/metrics/collector/automation.go (2)

28-32: High-cardinality label consideration with rule_name.

Including rule_name as a Prometheus label can lead to high cardinality if users frequently rename rules or create many rules over time. Old label combinations persist until the process restarts. This is acceptable for a moderate number of rules but worth documenting as a known characteristic.


34-118: LGTM - Clean metric registration pattern.

The collector initialization is well-structured with consistent naming, appropriate help text, and correct label usage. The explicit registration of each metric provides clarity.

cmd/qui/main.go (1)

711-713: LGTM - Clean metrics wiring integration.

The metrics collector is correctly injected into the automation service only when metrics are enabled, following the established pattern in the codebase. The placement after MetricsManager creation and before server start is appropriate.

internal/services/automations/service.go (2)

535-547: LGTM - Defensive nil check and clean iteration.

The nil check for metricsCollector ensures graceful behavior when metrics are disabled. The iteration pattern correctly matches rules to their stats.


663-665: Good use of defer for metrics collection.

Using defer ensures metrics are collected regardless of how the function exits (early returns, errors, or normal completion). This guarantees consistent metric emission for each rule run.

internal/metrics/metrics.go (2)

32-32: Verify: AutomationCollector self-registers its metrics.

Unlike TorrentCollector (which implements the Collector interface and is registered via registry.MustRegister), AutomationCollector internally registers each CounterVec in its constructor using r.MustRegister(...). This is a valid pattern, but it's worth noting the design difference.

This means AutomationCollector doesn't need to implement Describe/Collect since its metrics are individually registered. This works correctly but differs from the TorrentCollector pattern.


47-50: LGTM - Clean accessor method.

The accessor follows the existing pattern with GetRegistry() and provides a straightforward way to retrieve the automation collector.

internal/services/automations/processor_test.go (2)

144-190: LGTM - Comprehensive unit test for CollectMetrics.

The test thoroughly validates all 16 metrics are correctly incremented with expected values. The use of testutil.ToFloat64 is the standard approach for asserting Prometheus counter values in tests.


192-249: LGTM - Good integration test with negative assertions.

The test validates the end-to-end flow from processTorrents through CollectMetrics, and importantly verifies that unrelated metrics remain at zero. This negative testing pattern helps catch unintended side effects.

internal/services/automations/processor.go (1)

67-85: LGTM - Clean metrics emission implementation.

The method correctly uses Inc() for the total count and Add() for the individual stat counters. The label generation via GetAutomationRuleRunLabels ensures consistency with the collector's label definition.

The lack of a nil check here is acceptable since the caller (collectRuleRunMetrics in service.go) guards against nil before invoking this method.

internal/metrics/manager_test.go (2)

62-69: LGTM - Good accessor test coverage.

The test validates both that the accessor returns a non-nil value and that it's the correct type.


174-188: LGTM - Validates metric descriptor count.

The test correctly uses r.Describe() on the registry (since AutomationCollector's metrics are individually registered) and verifies all 16 metric descriptors are present.

Comment thread internal/metrics/collector/automation.go Outdated
Comment thread internal/services/automations/processor.go Outdated
Comment thread internal/metrics/collector/automation.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 765c7d7 and 29db835.

📒 Files selected for processing (5)
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/service.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/services/automations/service.go
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-11-25T22:46:03.762Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.

Applied to files:

  • internal/services/automations/processor_test.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
🧬 Code graph analysis (3)
internal/services/automations/processor_test.go (2)
internal/models/automation.go (3)
  • Automation (34-46)
  • ActionConditions (448-456)
  • CategoryAction (498-506)
internal/metrics/collector/automation.go (1)
  • NewAutomationCollector (14-33)
internal/services/automations/processor.go (1)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-12)
internal/metrics/manager_test.go (2)
internal/metrics/metrics.go (1)
  • NewMetricsManager (22-42)
internal/metrics/collector/automation.go (2)
  • AutomationCollector (9-12)
  • NewAutomationCollector (14-33)
🔇 Additional comments (4)
internal/metrics/manager_test.go (1)

59-60: LGTM!

The test coverage for the new AutomationCollector integration is thorough and follows the existing test patterns in the file. The tests appropriately validate registration, accessor methods, and metric descriptor counts.

Also applies to: 62-69, 174-189

internal/services/automations/processor.go (1)

67-85: LGTM!

The CollectMetrics implementation correctly translates the ruleRunStats counters into Prometheus metrics. The use of MustCurryWith for pre-binding instance and rule labels is elegant and efficient.

internal/metrics/collector/automation.go (2)

14-33: Well-designed metrics following Prometheus best practices.

The implementation correctly:

  • Uses _total suffix for accumulating counters per Prometheus naming conventions
  • Includes both instance_name and rule_name labels for readable dashboards (addressing previous feedback)
  • Registers metrics with appropriate namespace/subsystem structure

44-51: Elegant use of MustCurryWith for efficient label binding.

Pre-binding the instance and rule labels via MustCurryWith is an excellent design choice. It allows callers to efficiently add multiple action-labeled metrics without repeating the common labels for each action type. This pattern is well-suited to the usage in processor.go where multiple action metrics are emitted per rule run.

Comment thread internal/services/automations/processor_test.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c74d18c and bdcaded.

📒 Files selected for processing (2)
  • internal/services/automations/processor.go
  • internal/services/automations/service.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/processor.go
  • internal/services/automations/service.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/processor.go
  • internal/services/automations/service.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/services/automations/processor.go
  • internal/services/automations/service.go
🧬 Code graph analysis (2)
internal/services/automations/processor.go (2)
internal/models/automation.go (1)
  • Automation (34-46)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-12)
internal/services/automations/service.go (1)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-12)
🔇 Additional comments (6)
internal/services/automations/processor.go (1)

11-11: LGTM!

The collector import is correctly added to support metrics collection.

internal/services/automations/service.go (5)

24-24: LGTM!

The collector import is correctly added.


66-66: LGTM!

The metrics collector field is appropriately added to the Service struct.


99-101: LGTM!

The setter enables dependency injection of the metrics collector, allowing the service to remain testable without requiring metrics infrastructure.


532-544: LGTM!

The helper properly handles nil collectors and iterates through rules to emit metrics. The nil-check prevents panics when metrics are disabled.


661-661: LGTM!

The defer placement ensures metrics are collected after rule processing completes, capturing the final state of all rule statistics.

Comment thread internal/services/automations/processor.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/services/automations/processor.go (1)

66-77: LGTM! Well-aligned with the outcome-focused metrics design.

The implementation correctly emits metrics for actions taken rather than conditions evaluated, which aligns with the extensive design discussion in past reviews. The metric updates accurately reflect the rule execution outcomes.

Optional: Add defensive nil check

For robustness, consider adding a nil check at the start of the method:

 func (s *ruleRunStats) CollectMetrics(rule *models.Automation, metricsCollector *collector.AutomationCollector, instanceName string) {
+	if metricsCollector == nil {
+		return
+	}
 	metricsCollector.GetAutomationRuleRunTotal(rule.InstanceID, instanceName, rule.ID, rule.Name).Inc()

This prevents panics if metrics collection is disabled and the service layer passes nil.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdcaded and b72e99b.

📒 Files selected for processing (4)
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/metrics/manager_test.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/services/automations/processor.go
  • internal/metrics/collector/automation.go
  • internal/services/automations/processor_test.go
🧬 Code graph analysis (2)
internal/services/automations/processor.go (2)
internal/models/automation.go (1)
  • Automation (34-46)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-13)
internal/services/automations/processor_test.go (3)
internal/models/automation.go (3)
  • Automation (34-46)
  • ActionConditions (448-456)
  • CategoryAction (498-506)
internal/metrics/collector/automation.go (1)
  • NewAutomationCollector (15-41)
internal/qbittorrent/sync_manager.go (1)
  • NewSyncManager (216-237)
🔇 Additional comments (5)
internal/services/automations/processor_test.go (2)

144-182: LGTM! Comprehensive test coverage.

The test thoroughly validates that CollectMetrics correctly increments each metric type with the expected values from the stats fields.


184-237: LGTM! End-to-end metrics flow validated correctly.

The test verifies the complete flow from torrent processing through metrics collection, correctly asserting both the category action that was applied and the action types that remained at zero.

internal/metrics/collector/automation.go (3)

9-41: LGTM! Well-structured collector following Prometheus conventions.

The AutomationCollector properly implements the Prometheus metrics pattern with:

  • Correct _total suffix for all counter metrics
  • Appropriate label dimensions for dashboarding (instance_id/name, rule_id/name)
  • Clean registration via MustRegister

43-59: LGTM! Accessor methods correctly return fully-labeled counters.

Both methods properly bind all label dimensions and return ready-to-use prometheus.Counter instances.


61-68: LGTM! Correct use of currying for multi-label metric.

The method properly uses MustCurryWith to pre-bind instance and rule labels, allowing the caller to specify the action label. This pattern is used correctly in processor.go.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b72e99b and d395efa.

📒 Files selected for processing (5)
  • internal/metrics/collector/automation.go
  • internal/metrics/manager_test.go
  • internal/services/automations/processor.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/service.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • internal/services/automations/service.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • internal/services/automations/service.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/services/automations/service.go
  • internal/metrics/manager_test.go
  • internal/services/automations/processor_test.go
  • internal/services/automations/processor.go
  • internal/metrics/collector/automation.go
🧬 Code graph analysis (2)
internal/services/automations/processor_test.go (2)
internal/models/automation.go (3)
  • Automation (34-46)
  • ActionConditions (448-456)
  • CategoryAction (498-506)
internal/metrics/collector/automation.go (1)
  • NewAutomationCollector (16-49)
internal/services/automations/processor.go (1)
internal/metrics/collector/automation.go (1)
  • AutomationCollector (9-14)
🔇 Additional comments (10)
internal/services/automations/processor_test.go (2)

144-187: LGTM!

Comprehensive test coverage for the new CollectMetrics method. The test validates all metric increments for both action-performed and action-not-performed counters.


189-246: LGTM!

The test correctly validates the full metrics emission flow through processTorrents and CollectMetrics. Past review concerns have been addressed—the redundant manual Add call was removed and the category_applied assertion is now present.

internal/metrics/manager_test.go (3)

59-59: LGTM!

Correctly added assertion to verify AutomationCollector is registered, consistent with the existing pattern for TorrentCollector.


62-69: LGTM!

Standard test pattern for the new GetAutomationCollector accessor, matching the existing test structure for other collectors.


174-189: LGTM!

Correctly validates that AutomationCollector exposes the expected 4 metric descriptors (RuleRunTotal, RuleRunTorrentsMatchedTotal, RuleRunActionTotal, RuleRunActionNotPerformedTotal).

internal/services/automations/processor.go (3)

11-11: LGTM!

Correctly imports the new metrics collector package.


61-62: LGTM!

Correctly splits the category metrics into distinct counters for "condition not met" vs "blocked by cross-seed protection", addressing the past review feedback about distinguishing these cases.


67-82: LGTM!

The CollectMetrics method correctly emits all automation metrics, properly categorizing actions performed vs not performed. The implementation aligns with the new split category metrics.

internal/services/automations/service.go (1)

24-24: LGTM!

Clean integration of metrics collection into the automation service:

  • Proper dependency injection via SetMetricsCollector
  • Metrics emission deferred until after rule processing completes
  • Nil guard prevents issues when metrics are disabled
  • Logging updated to reflect the split category metrics

Also applies to: 66-66, 99-101, 532-544, 661-661, 692-693

internal/metrics/collector/automation.go (1)

1-85: LGTM!

Excellent implementation of the AutomationCollector following Prometheus best practices:

  • Counter names properly suffixed with _total
  • Consistent label sets across metrics
  • Correct use of MustCurryWith for metrics with additional action labels
  • Clean constructor pattern with registry registration
  • Descriptive help text for all metrics

Comment thread internal/services/automations/processor.go
Comment thread internal/services/automations/processor.go Outdated
@s0up4200 s0up4200 changed the base branch from main to develop January 2, 2026 07:27
@boxhock boxhock requested a review from finevan January 2, 2026 15:55
Comment on lines +24 to +29
RuleRunTorrentsMatchedTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "qui",
Subsystem: "automation",
Name: "rule_run_torrents_matched_total",
Help: "Total number of torrents that matched the trackers in the rule",
}, []string{"instance_id", "instance_name", "rule_id", "rule_name"}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on updating this to a guage metric? This way it shows us the most recent number of torrents matched any time it's reported, and we don't have to work backwards from the slope. If the reporting interval is less frequent than the rule execution interval, it won't necessarily tell us exactly how many items were matched every run, however I think that's also true in many cases with an accumulating counter. and the gauge is easier to understand.

Suggested change
RuleRunTorrentsMatchedTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "qui",
Subsystem: "automation",
Name: "rule_run_torrents_matched_total",
Help: "Total number of torrents that matched the trackers in the rule",
}, []string{"instance_id", "instance_name", "rule_id", "rule_name"}),
RuleRunTorrentsMatchedTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "qui",
Subsystem: "automation",
Name: "rule_run_torrents_matched",
Help: "Total number of torrents that matched the trackers in the rule",
}, []string{"instance_id", "instance_name", "rule_id", "rule_name"}),

We'd need to update processor.go

- metricsCollector.GetAutomationRuleRunTorrentsMatchedTotal(rule.InstanceID, instanceName, rule.ID, rule.Name).Add(float64(s.MatchedTrackers))
+ metricsCollector.GetAutomationRuleRunTorrentsMatchedTotal(rule.InstanceID, instanceName, rule.ID, rule.Name).Set(float64(s.MatchedTrackers))

Copy link
Copy Markdown
Contributor

@finevan finevan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really excited to try this out. Added one more minor thought, but I trust your judgement on whether or not it's worthwhile

@s0up4200 s0up4200 self-requested a review January 16, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants