Skip to content

Conversation

@FarisZR
Copy link
Owner

@FarisZR FarisZR commented Oct 27, 2025

This PR adds JSON logs to make integrating knocker-cli with other wrappers easier like a gnome extension.

Summary by CodeRabbit

  • New Features

    • Structured event logging system with schema versioning for enhanced observability
    • Service now emits lifecycle, status, whitelist, next-knock, knock-trigger, and error events
    • Manual knock invocations now emit success and failure events with metadata
  • Documentation

    • Added comprehensive guides for structured events, logging schema, event catalogue, and how to consume/stream events
  • Tests

    • Tests updated to use the public service constructor and public shutdown API

@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds a journald-based structured event system and emits versioned KNOCKER_* events across service lifecycle, knock operations, and whitelist management. Introduces a platform-agnostic journald wrapper, integrates event emission into Service and CLI manual-knock paths, updates service lifecycle synchronization, and expands documentation for the event contract.

Changes

Cohort / File(s) Summary
Journald infrastructure
internal/journald/journald.go, internal/journald/journald_linux.go, internal/journald/journald_stub.go
New package exposing Priority, Fields, SchemaVersion, Enabled(), and Emit(); Linux implementation uses coreos/go-systemd; non-Linux stub is no-op; caches disabled state on specific syscall errors.
Service event logging
internal/service/event_logging.go
New emitters and helpers producing structured KNOCKER_* events (ServiceState, StatusSnapshot, WhitelistApplied/Expired, NextKnockUpdated, KnockTriggered, Error) and centralized emit path.
Service core & lifecycle
internal/service/service.go, internal/service/service_test.go
NewService constructor now accepts version; Service gains version, whitelist/nextKnock state, stopOnce/shutdownOnce; Run/Stop and knock flows updated to emit events, perform IP/health checks, and manage whitelist lifecycle; tests updated to use constructor/Stop().
CLI manual knock
cmd/knocker/knock.go
Emits structured success/failure events for manual knock via new helpers; adds journald-related imports and message field construction.
Program lifecycle management
cmd/knocker/program.go
Adds mu (sync.RWMutex) and service pointer to program struct; run/start/stop logic updated to store/cleanup service under locks; NewService call updated to include version.
Documentation
README.md, docs/architecture.md, docs/logging.md
Adds documentation describing structured journald events, schema versioning, event catalogue, consumption guidance, and examples.
Dependencies
go.mod
Adds direct requirements: github.com/coreos/go-systemd/v22 v22.5.0 and github.com/spf13/pflag v1.0.6.
CLI / program imports & minor edits
cmd/knocker/*
Minor phrasing and logging changes in README and CLI flow to reflect structured event emission and small wording tweaks.

Sequence Diagram(s)

sequenceDiagram
    actor CLI as CLI/User
    participant prog as program
    participant svc as Service
    participant journald as journald.Emit

    CLI->>prog: knocker knock (manual)
    prog->>svc: Start / NewService(version)
    svc->>journald: Emit(ServiceState="started")

    alt manual knock
        CLI->>svc: trigger manual knock
        svc->>journald: Emit(KnockTriggered, TriggerSource="cli")
        alt success
            svc->>journald: Emit(WhitelistApplied)
        else failure
            svc->>journald: Emit(Error)
        end
    end

    rect rgb(230,245,255)
        Note over svc: scheduled loop
        svc->>svc: maybe fetch public IP
        alt IP changed
            svc->>svc: health check -> performKnock()
            svc->>journald: Emit(KnockTriggered / WhitelistApplied or Error)
        else unchanged
            svc->>journald: Emit(StatusSnapshot)
        end
    end

    CLI->>prog: Stop
    svc->>journald: Emit(ServiceState="stopping")
    svc->>journald: Emit(ServiceState="stopped")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • internal/journald/journald.go: disableOn syscall error checks and atomic disabled caching.
    • internal/service/service.go & event_logging.go: correctness and ordering of emitted fields, nextKnockUnix and whitelist expiry logic, concurrency synchronization (stopOnce/shutdownOnce).
    • cmd/knocker/program.go and cmd/knocker/knock.go: mutex usage and service lifecycle interplay with manual knock emissions.
    • go.mod dependency addition: ensure coreos/go-systemd usage is correct and build tags behave as expected.

Possibly related PRs

Poem

🐰 Click-click, logs all bright and clear,

KNOCKER fields now hop and cheer,
Schema set and events in line,
Whitelists, knocks, and states align,
A rabbit's note: observability is here! 🎩

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The PR title claims "add new Json logs," but the actual implementation adds structured journald event logging via systemd's journald system, not direct JSON output. While the PR objectives mention "JSON logs," the changeset introduces a journald wrapper (internal/journald package), journald-based event emission in the service layer, and structured key-value fields with KNOCKER_* identifiers. The title is misleading about the technical approach—a teammate reviewing the commit history would expect JSON logging functionality rather than journald-based structured events, which are consumed differently (via journalctl). This mismatch between the stated title and the actual implementation undermines clarity. Consider revising the title to accurately reflect the implementation, such as "add structured journald event logging" or "add structured event emission via journald." This would be clearer for reviewers and future developers examining the commit history, as it correctly conveys that this PR integrates systemd journald for structured logging rather than direct JSON log output.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch json-logs

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

@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

🧹 Nitpick comments (7)
docs/architecture.md (1)

69-72: Fix list indentation for markdown style consistency.

The nested list items use 4-space indentation instead of the expected 2 spaces.

Apply this diff to fix the indentation:

-- The core service emits:
-    - `ServiceState` when entering `started`, `stopping`, or `stopped` transitions (including `KNOCKER_VERSION`).
-    - `StatusSnapshot` whenever material state changes (whitelist, TTL, next knock timestamp) so consumers can seed their UI.
-    - `WhitelistApplied`, `WhitelistExpired`, `NextKnockUpdated`, and `KnockTriggered` as the whitelist lifecycle evolves.
-    - `Error` whenever a problem (IP lookup, health check, knock) should surface in the UI, tagged with `KNOCKER_ERROR_CODE`.
+- The core service emits:
+  - `ServiceState` when entering `started`, `stopping`, or `stopped` transitions (including `KNOCKER_VERSION`).
+  - `StatusSnapshot` whenever material state changes (whitelist, TTL, next knock timestamp) so consumers can seed their UI.
+  - `WhitelistApplied`, `WhitelistExpired`, `NextKnockUpdated`, and `KnockTriggered` as the whitelist lifecycle evolves.
+  - `Error` whenever a problem (IP lookup, health check, knock) should surface in the UI, tagged with `KNOCKER_ERROR_CODE`.
go.mod (1)

6-6: Update go-systemd dependency to v22.6.0.

v22.5.0 is outdated; v22.6.0 is the current stable release (released August 20, 2025). No security vulnerabilities were detected in the go-systemd repository. v22.6.0 includes bug fixes (thread-safe error handling in dlopen, C wrapper fixes), a new dbus method, and bumps the Go requirement to 1.23—verify this aligns with your project's Go version support before updating.

cmd/knocker/program.go (1)

64-71: Remove redundant NotifyStopping; Stop() already emits it.

Service.Stop calls NotifyStopping internally. Calling both is harmless due to sync.Once but redundant.

-  if svc != nil {
-    svc.NotifyStopping()
-    svc.Stop()
-  }
+  if svc != nil {
+    svc.Stop()
+  }
internal/service/service.go (1)

72-89: Deduplicate NotifyStopping calls to reduce noise.

You call NotifyStopping in:

  • Run quit case,
  • Run s.stop case,
  • Service.Stop.

sync.Once protects against duplicate emission, but you can remove one set (e.g., from Run) to simplify reasoning. Not required, just cleaner.

If Run must support external quit without invoking Service.Stop, keep the NotifyStopping in Run and rely on Stop’s call only when Stop() is used.

internal/service/event_logging.go (2)

51-55: Auto-attach KNOCKER_VERSION in emit() so all events carry version.

Currently only ServiceState includes version. Centralizing avoids drift.

 func (s *Service) emit(eventType, message string, priority journald.Priority, fields journald.Fields) {
-  if err := journald.Emit(eventType, message, priority, fields); err != nil && s.Logger != nil {
+  // Copy to avoid mutating caller’s map.
+  if fields == nil {
+    fields = journald.Fields{}
+  } else {
+    dup := make(journald.Fields, len(fields)+1)
+    for k, v := range fields { dup[k] = v }
+    fields = dup
+  }
+  if s.version != "" {
+    fields["KNOCKER_VERSION"] = s.version
+  }
+  if err := journald.Emit(eventType, message, priority, fields); err != nil && s.Logger != nil {
     s.Logger.Printf("Failed to emit journald event %s: %v", eventType, err)
   }
 }

91-105: Unify source field naming across events.

Whitelist uses KNOCKER_SOURCE while Knock uses KNOCKER_TRIGGER_SOURCE. Consider standardizing to a single key to simplify consumers (before locking schema v1).

Does docs/logging.md recommend one of these? Align code with docs if already defined.

Also applies to: 154-161

internal/journald/journald.go (1)

36-41: Short-circuit Emit when journald isn’t available.

Avoid calling emit() on platforms where Enabled() is false.

 func Emit(eventType, message string, priority Priority, fields Fields) error {
-  if journaldDisabled.Load() {
+  if journaldDisabled.Load() || !Enabled() {
     return nil
   }

Also applies to: 44-76

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39aa322 and 48243d4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • README.md (3 hunks)
  • cmd/knocker/knock.go (3 hunks)
  • cmd/knocker/program.go (3 hunks)
  • docs/architecture.md (1 hunks)
  • docs/logging.md (1 hunks)
  • go.mod (1 hunks)
  • internal/journald/journald.go (1 hunks)
  • internal/journald/journald_linux.go (1 hunks)
  • internal/journald/journald_stub.go (1 hunks)
  • internal/service/event_logging.go (1 hunks)
  • internal/service/service.go (4 hunks)
  • internal/service/service_test.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
internal/service/service.go (3)
internal/api/client.go (2)
  • Client (11-15)
  • KnockResponse (17-21)
internal/util/ip.go (1)
  • IPGetter (9-11)
internal/service/event_logging.go (9)
  • ServiceStateStarted (22-22)
  • ServiceStateStopped (24-24)
  • ServiceStateStopping (23-23)
  • TriggerSourceSchedule (29-29)
  • ErrorCodeIPLookup (39-39)
  • ErrorCodeHealthCheck (40-40)
  • ResultFailure (35-35)
  • ErrorCodeKnockFailed (41-41)
  • ResultSuccess (34-34)
cmd/knocker/knock.go (3)
internal/journald/journald.go (4)
  • Emit (47-76)
  • PriErr (18-18)
  • Fields (27-27)
  • PriInfo (21-21)
internal/service/event_logging.go (7)
  • EventKnockTriggered (17-17)
  • TriggerSourceCLI (28-28)
  • ResultFailure (35-35)
  • EventError (18-18)
  • ErrorCodeKnockFailed (41-41)
  • ResultSuccess (34-34)
  • EventWhitelistApplied (14-14)
internal/api/client.go (1)
  • KnockResponse (17-21)
internal/journald/journald_stub.go (1)
internal/journald/journald.go (2)
  • Priority (11-11)
  • Fields (27-27)
internal/service/service_test.go (2)
internal/service/service.go (1)
  • NewService (34-45)
internal/api/client.go (1)
  • NewClient (23-29)
internal/journald/journald_linux.go (1)
internal/journald/journald.go (3)
  • Enabled (38-40)
  • Priority (11-11)
  • Fields (27-27)
internal/service/event_logging.go (2)
internal/service/service.go (1)
  • Service (16-32)
internal/journald/journald.go (6)
  • Priority (11-11)
  • Fields (27-27)
  • Emit (47-76)
  • PriInfo (21-21)
  • PriNotice (20-20)
  • PriErr (18-18)
cmd/knocker/program.go (1)
internal/service/service.go (2)
  • Service (16-32)
  • NewService (34-45)
🪛 LanguageTool
README.md

[grammar] ~12-~12: Ensure spelling is correct
Context: ...ist request at any time. - Structured journald events: Emits machine-readable journa...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~12-~12: Ensure spelling is correct
Context: ...urnald events:** Emits machine-readable journald events alongside human logs for desktop...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~89-~89: Ensure spelling is correct
Context: ...the configured interval. ## Structured journald events When the service runs under sys...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~100-~100: ‘with success’ might be wordy. Consider a shorter alternative.
Context: ...cli) and scheduled (schedule) knocks with success/failure result. - Error` — surfaced ...

(EN_WORDINESS_PREMIUM_WITH_SUCCESS)

docs/architecture.md

[grammar] ~63-~63: Ensure spelling is correct
Context: ...for easy deployment. ### 8. Structured Journald Events Linux deployments run the servi...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
docs/architecture.md

69-69: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


70-70: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


71-71: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


72-72: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
internal/journald/journald_stub.go (1)

1-11: LGTM! Clean cross-platform stub implementation.

The build-tagged stub correctly provides no-op behavior for non-Linux platforms, ensuring the codebase remains portable.

README.md (2)

12-12: Excellent addition of structured journald events feature.

The new feature documentation clearly explains the machine-readable event integration for desktop environments.


89-103: Well-documented journald integration with practical examples.

The structured events section provides clear guidance on schema versioning, event types, and consumption patterns with journalctl. The note about manual invocations producing the same events ensures consistency for external consumers.

docs/architecture.md (1)

63-76: Comprehensive documentation of the journald integration architecture.

The new section clearly explains the journald abstraction, event schema, and cross-platform handling. The description of event types and consumption patterns provides valuable context for understanding the implementation.

cmd/knocker/knock.go (3)

33-33: Good integration of structured event emission into the knock flow.

The manual knock command now properly emits KnockTriggered and WhitelistApplied events, maintaining consistency with the background service's event stream.

Also applies to: 37-37


48-59: Well-structured failure event emission.

The function correctly emits both KnockTriggered (with failure result) and Error events, providing comprehensive observability for manual knock failures. Ignoring journald errors is appropriate since event emission is best-effort and shouldn't block the main error path.


61-105: Thorough success event emission with proper nil handling.

The function safely handles nil knockResponse and conditionally populates fields only when values are available. The structured event emission for both KnockTriggered and WhitelistApplied ensures external consumers receive complete state updates.

internal/service/service_test.go (1)

43-51: Excellent refactoring to use public API instead of internal details.

The test now uses NewService constructor and Stop() method rather than directly manipulating internal fields and channels. This improves test maintainability and respects the service's encapsulation.

Also applies to: 63-63

internal/journald/journald_linux.go (2)

9-11: Clean delegation to the systemd journal library.

The enabled() function correctly delegates to journal.Enabled() for Linux platforms.


13-20: Efficient and correct journald emission implementation.

The function pre-allocates the payload map for efficiency, safely copies all fields, and properly type-casts the priority before calling journal.Send. Error handling is appropriate with direct propagation.

docs/logging.md (1)

1-118: Outstanding structured logging documentation.

This comprehensive guide covers the complete journald event contract including schema versioning, all event types with detailed field descriptions, practical JSON examples, and parser recommendations. The documentation will be invaluable for developers building integrations with Knocker.

cmd/knocker/program.go (1)

44-47: Confirm fatal-on-healthcheck is intended for service mode.

logger.Fatalf will terminate the whole process from the run goroutine. If you prefer graceful startup failure (so the service manager can retry), return an error instead and let the supervisor handle restarts.

internal/service/service.go (3)

47-65: Run lifecycle sequencing looks solid.

Startup emits (state → nextKnock → snapshot) and deferred teardown (clear next → snapshot → stopped) are correct and will keep consumers consistent.


145-162: Good centralization of knock side effects.

performKnock encapsulates API call, success/failure emits, and whitelist handling. Clear and testable.


180-208: Expiry handling is correct and observable.

Edge cases (missing IP/expiredUnix) are handled, and both human logs and structured events are emitted.

Copy link

@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 (2)
cmd/knocker/program.go (2)

47-51: Avoid Fatalf in goroutine; prefer graceful start failure.

logger.Fatalf will exit the process from a worker goroutine. Return an error from Start (after doing the health check there) or bail out of run cleanly without terminating the whole service.

Example (minimal) inside run to avoid process exit:

-	if err := apiClient.HealthCheck(); err != nil {
-		logger.Fatalf("Initial health check failed: %v. Please check your API URL and key.", err)
-	}
+	if err := apiClient.HealthCheck(); err != nil {
+		logger.Printf("Initial health check failed: %v. Please check your API URL and key.", err)
+		return
+	}

Better: move the health check into Start and return the error so the service manager handles it.


68-81: Close channel outside the lock to minimize lock hold time.

Closing while holding the lock is safe, but swapping then closing after unlock avoids holding the lock during the close path.

Apply:

-	p.mu.Lock()
-	if p.quit != nil {
-		close(p.quit)
-		p.quit = nil
-	}
-	p.mu.Unlock()
+	p.mu.Lock()
+	quit := p.quit
+	p.quit = nil
+	p.mu.Unlock()
+	if quit != nil {
+		close(quit)
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48243d4 and d039deb.

📒 Files selected for processing (1)
  • cmd/knocker/program.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
cmd/knocker/program.go (1)
internal/service/service.go (2)
  • Service (16-32)
  • NewService (34-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
cmd/knocker/program.go (5)

15-17: Good state encapsulation and locking primitives.

Adding mu (RWMutex) and service pointer is appropriate for coordinating lifecycle access.


22-28: Race on p.quit resolved; launch pattern looks correct.

Initializing p.quit under Lock and passing a captured quit into run removes the read/write race noted earlier. Nice.


55-62: Service pointer lifecycle guarded correctly.

Assigning and clearing p.service under the mutex is sound; Stop reads it under RLock, which pairs well with this.


64-64: Run(quit) handoff is clean.

Passing quit explicitly avoids future accidental unsynchronized reads of p.quit.


53-54: Version parameter is already properly wired—no action needed.

The version constant is already defined at the package level in cmd/knocker/main.go:17 as version = "dev", and the code at line 53 correctly passes it to NewService. The original concern about missing the version symbol is unfounded; compilation will succeed without any changes.

Likely an incorrect or invalid review comment.

@FarisZR FarisZR merged commit cad7597 into main Oct 27, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants