feat(observability): event triggers + log scanner backend
Two paired backends sharing the events.Bus seam:
Event triggers (consumer-side):
- internal/store/event_triggers.go — CRUD with action_secret
redaction on read (placeholder echo treated as "no change" on
PATCH so secrets aren't accidentally wiped).
- internal/events/dispatcher.go — bus subscriber, AND-composed
filters (severity CSV, source CSV, message regex with memoized
compile cache). Structural loop-prevention: never writes to
event_log. Sends via notifier.SendPayload.
- internal/notify: SendPayload + SendSyncForTestPayload methods,
TierEventTrigger constant, doSendRaw shared with the legacy
Event-shaped path.
- internal/api/event_triggers.go — admin-gated CRUD + /test
sending the real TriggerWebhookPayload shape. SSRF guard
rejects loopback / link-local / unspecified targets. PATCH
uses pointer-typed DTO for partial updates.
Log scanner (producer-side):
- internal/logscanner/ — engine (per-rule cooldown +
per-container token bucket, atomic drop counters), tail
(multiplexed docker frame demuxer with TTY fallback + 16 MiB
payload cap + 1 MiB reassembly cap + RFC3339Nano-validated
timestamp strip + UTF-8-safe message truncation), manager
(5s container polling, atomic.Pointer[Snapshot] hot-reload,
HitEmitter writes event_log + publishes EventLog so the
trigger dispatcher picks them up immediately).
- internal/docker/container.go — ContainerLogsOpts exposes
stream selection for stderr-only / stdout-only rules.
- internal/store: log_scan_rules table + CRUD with
EffectiveLogScanRules resolver (globals minus per-workload
overrides plus workload-only additions). Transactional
cascade-delete of overrides when a global rule is removed.
- internal/api/log_scan_rules.go — admin-gated CRUD + /test
(sample_line → matched/captures) + /stats (drop counters +
active tail count + last-snapshot compile errors) +
GET /api/workloads/{id}/effective-rules.
cmd/server/main.go wires both subsystems next to the existing
RegisterPersistentLogger. Coverage spans engine cooldown / bucket
counter tests, snapshot effective-set semantics, manager compile-
error capture, dispatcher matching, store validation +
cascade-delete, API URL validator + secret redaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -359,12 +359,41 @@ func isTinyforgeManaged(labels map[string]string) bool {
|
||||
// ContainerLogs returns a log stream for a container.
|
||||
// If follow is true, the stream stays open for new log lines.
|
||||
// tail specifies the number of lines from the end to return (e.g., "200").
|
||||
// Both stdout and stderr are streamed. For stream-selective reads
|
||||
// (e.g. the log scanner narrowing to stderr-only), use ContainerLogsOpts.
|
||||
func (c *Client) ContainerLogs(ctx context.Context, containerID string, follow bool, tail string) (io.ReadCloser, error) {
|
||||
result, err := c.api.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
return c.ContainerLogsOpts(ctx, containerID, ContainerLogOptions{
|
||||
Follow: follow,
|
||||
Tail: tail,
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ContainerLogOptions controls which streams + framing are pulled
|
||||
// from a container. Currently expanded over the legacy ContainerLogs
|
||||
// shape so the log-scanner can read stderr-only rules without
|
||||
// post-filtering every line.
|
||||
type ContainerLogOptions struct {
|
||||
Follow bool
|
||||
Tail string
|
||||
ShowStdout bool
|
||||
ShowStderr bool
|
||||
}
|
||||
|
||||
// ContainerLogsOpts is the stream-selectable counterpart to
|
||||
// ContainerLogs. When both ShowStdout and ShowStderr are false the
|
||||
// upstream client returns an empty stream — we treat that as caller
|
||||
// error and return an explicit message rather than a silent no-op.
|
||||
func (c *Client) ContainerLogsOpts(ctx context.Context, containerID string, opts ContainerLogOptions) (io.ReadCloser, error) {
|
||||
if !opts.ShowStdout && !opts.ShowStderr {
|
||||
return nil, fmt.Errorf("container logs %s: at least one of ShowStdout/ShowStderr must be true", containerID)
|
||||
}
|
||||
result, err := c.api.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{
|
||||
ShowStdout: opts.ShowStdout,
|
||||
ShowStderr: opts.ShowStderr,
|
||||
Follow: opts.Follow,
|
||||
Tail: opts.Tail,
|
||||
Timestamps: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user