Skip to content

feat(observ): Add observability engine, plugin tool bridge, notifications#165

Merged
nfebe merged 1 commit into
mainfrom
feat/observability-plugin-platform
Jul 6, 2026
Merged

feat(observ): Add observability engine, plugin tool bridge, notifications#165
nfebe merged 1 commit into
mainfrom
feat/observability-plugin-platform

Conversation

@nfebe

@nfebe nfebe commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Delivers the observability and self-recovery item (#148) as a native FlatRun capability. It runs as a plugin but off the same agent binary (the agent re-execs itself into a hidden plugin subcommand), so there is one binary to ship and no per-arch plugin builds.

Per-container metrics are collected under OpenTelemetry container semantic-convention names and kept in a bounded history window for the UI, so the same data can later go over OTLP to any OpenTelemetry backend without a rename.

Self-healing is the part to review. It only acts on FlatRun-managed deployments (directories under the data path): an earlier build treated every host container as managed and restart-looped an unrelated external one. External containers are now read for display and never restarted, and a managed container is retried at most three times with a cooldown before giving up.

Two new extension points:

  • Plugins can contribute tools the AI assistant can call, namespaced per plugin. A tool that mutates state is gated on write access to the target deployment, and a container-scoped restart refuses a container outside the session's deployment. That gate is the security boundary worth scrutiny.
  • Notifications are core, not plugin-specific: admins configure email or webhook targets and plugins emit into it; observability's self-heal is the first emitter. Delivery goes through shoutrrr.

Follow-ups tracked in #158, none blocking:

  • The health watcher holds its lock across the docker restart call, briefly blocking health reads during a restart.
  • Notification target URLs, including SMTP passwords, are stored in cleartext in the runtime config, consistent with the existing flat-file secret handling.

…ions

FlatRun now ships a native observability capability built as a plugin that
runs in-process off the same agent binary. It collects per-container metrics
under OpenTelemetry container semantic-convention names, keeps a bounded
recent-history window for the UI, and watches container health.

Self-healing only ever acts on FlatRun-managed deployments (directories under
the data path). External containers that happen to share the host are read for
display but never auto-restarted, and a restarted container is retried at most
a few times with a cooldown between attempts, so a genuinely broken external
service can no longer trigger a restart loop.

Plugins can now contribute three things beyond routes: UI sections injected
into the deployments and settings views, a configuration schema, and tools the
AI assistant can call. Tool names are namespaced per plugin so they never
collide with built-ins. A tool that mutates state is gated on write access to
the target deployment, and a container-scoped action refuses to touch a
container outside the deployment the session is bound to.

Notifications are part of the core: admins configure delivery targets (email
or webhook), send a test, and plugins emit alerts into it (for example when a
container is auto-restarted). Delivery goes through shoutrrr.
@sourceant

sourceant Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review Summary

This PR introduces a robust observability and self-healing system integrated directly into the FlatRun agent. It features a plugin-based architecture using self-re-execution to simplify deployment across architectures. Key additions include a time-series metric engine, health monitoring with auto-recovery, a notification system via shoutrrr, and AI tool integration.

🚀 Key Improvements

  • Implemented a single-binary plugin distribution model using subcommands.
  • Added OpenTelemetry-aligned container metrics collection with in-memory retention.
  • Introduced a secure, deployment-scoped tool execution bridge for AI assistance.
  • Integrated shoutrrr for multi-channel notifications (email, webhooks).

💡 Minor Suggestions

  • Improve error handling in JSON encoders.
  • Optimize locking in the health watcher to prevent blocking reads during restarts.
  • Use ParseUint instead of ParseFloat for byte calculations to avoid precision issues.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread internal/api/server.go

func randomToken() string {
buf := make([]byte, 24)
if _, err := cryptoRand.Read(buf); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A failure in crypto/rand is extremely rare but usually indicates a catastrophic system failure. Panicking is acceptable here since the system cannot operate securely without a CSPRNG. However, ensure that this function is only called during startup and not dynamically in a way that could crash the running agent unexpectedly.

Suggested change
if _, err := cryptoRand.Read(buf); err != nil {
if _, err := cryptoRand.Read(buf); err != nil {
log.Fatalf("critical: crypto/rand unavailable: %v", err)
}

case strings.HasSuffix(s, "B"):
s = strings.TrimSuffix(s, "B")
}
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The use of strconv.ParseFloat here and casting back to uint64 could lead to precision loss for extremely large values, though unlikely for container memory. For consistency and precision with integer-based byte units, consider a more robust parsing approach.

Suggested change
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
v, _ := strconv.ParseUint(strings.TrimSpace(s), 10, 64)
return v * mult

Comment thread internal/observ/api.go

func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The json.NewEncoder can fail (though rare for these types). It is better to handle the error or at least log it, especially in a monitoring/observability tool.

Suggested change
_ = json.NewEncoder(w).Encode(v)
if err := json.NewEncoder(w).Encode(v); err != nil {
// Log error or handle appropriately
}

@nfebe nfebe merged commit 0a3fa5b into main Jul 6, 2026
5 checks passed
@nfebe nfebe deleted the feat/observability-plugin-platform branch July 6, 2026 16:17
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.

1 participant