Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions cmd/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"github.com/spf13/cobra"
clabevents "github.com/srl-labs/containerlab/core/events"
clabutils "github.com/srl-labs/containerlab/utils"
)

func eventsCmd(o *Options) (*cobra.Command, error) {
c := &cobra.Command{
Use: "events",
Short: "stream lab lifecycle and interface events",
Long: "stream container runtime events and interface updates for all running labs using the selected runtime\n" +
"reference: https://containerlab.dev/cmd/events/",
Aliases: []string{"ev"},
PreRunE: func(*cobra.Command, []string) error {
return clabutils.CheckAndGetRootPrivs()
},
RunE: func(cmd *cobra.Command, _ []string) error {
return eventsFn(cmd, o)
},
}

c.Flags().StringVarP(
&o.Events.Format,
"format",
"f",
o.Events.Format,
"output format. One of [plain, json]",
)

c.Flags().BoolVarP(
&o.Events.IncludeInitialState,
"initial-state",
"i",
o.Events.IncludeInitialState,
"emit the current container and interface states before streaming new events",
)

c.Example = `# Stream container and interface events in plain text
containerlab events

# Stream events as JSON
containerlab events --format json`

return c, nil
}

func eventsFn(cmd *cobra.Command, o *Options) error {
opts := clabevents.Options{
Format: o.Events.Format,
Runtime: o.Global.Runtime,
IncludeInitialState: o.Events.IncludeInitialState,
ClabOptions: o.ToClabOptions(),
Writer: cmd.OutOrStdout(),
}

return clabevents.Stream(cmd.Context(), opts)
}
9 changes: 9 additions & 0 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func GetOptions() *Options {
MermaidDirection: "TD",
DrawIOVersion: "latest",
},
Events: &EventsOptions{
Format: "plain",
},
ToolsAPI: &ToolsApiOptions{
Image: "ghcr.io/srl-labs/clab-api-server/clab-api-server:latest",
Name: "clab-api-server",
Expand Down Expand Up @@ -121,6 +124,7 @@ type Options struct {
Exec *ExecOptions
Inspect *InspectOptions
Graph *GraphOptions
Events *EventsOptions
ToolsAPI *ToolsApiOptions
ToolsCert *ToolsCertOptions
ToolsTxOffload *ToolsDisableTxOffloadOptions
Expand Down Expand Up @@ -353,6 +357,11 @@ type GraphOptions struct {
StaticDirectory string
}

type EventsOptions struct {
Format string
IncludeInitialState bool
}

type ToolsApiOptions struct {
Image string
Name string
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func subcommandRegisterFuncs() []func(*Options) (*cobra.Command, error) {
execCmd,
generateCmd,
graphCmd,
eventsCmd,
inspectCmd,
redeployCmd,
saveCmd,
Expand Down
108 changes: 108 additions & 0 deletions core/events/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package events

import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"time"
)

type formatter func(aggregatedEvent) error

func newFormatter(format string, w io.Writer) (formatter, error) {
normalized := strings.TrimSpace(strings.ToLower(format))
if normalized == "" {
normalized = "plain"
}

switch normalized {
case "plain":
return plainFormatter(w), nil
case "json":
return jsonFormatter(w), nil
default:
return nil, fmt.Errorf("output format %q is not supported, use 'plain' or 'json'", format)
}
}

func plainFormatter(w io.Writer) formatter {
return func(ev aggregatedEvent) error {
ts := ev.Timestamp
if ts.IsZero() {
ts = time.Now()
}
ts = ts.UTC()

actor := ev.ActorID
if actor == "" {
actor = ev.ActorName
}
if actor == "" {
actor = "-"
}

attrs := mergedEventAttributes(ev)
keys := make([]string, 0, len(attrs))
for k := range attrs {
keys = append(keys, k)
}
sort.Strings(keys)

parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, attrs[k]))
}

suffix := ""
if len(parts) > 0 {
suffix = " (" + strings.Join(parts, ", ") + ")"
}

_, err := fmt.Fprintf(w, "%s %s %s %s%s\n", ts.Format(time.RFC3339Nano), ev.Type, ev.Action, actor, suffix)

return err
}
}

func jsonFormatter(w io.Writer) formatter {
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)

return func(ev aggregatedEvent) error {
copy := ev
copy.Attributes = mergedEventAttributes(ev)

return encoder.Encode(copy)
}
}

func mergedEventAttributes(ev aggregatedEvent) map[string]string {
if len(ev.Attributes) == 0 && ev.ActorName == "" && ev.ActorFullID == "" {
return nil
}

attrs := make(map[string]string, len(ev.Attributes)+2)
for k, v := range ev.Attributes {
if v == "" {
continue
}

attrs[k] = v
}

if ev.ActorName != "" {
attrs["name"] = ev.ActorName
}

if ev.ActorFullID != "" {
attrs["id"] = ev.ActorFullID
}

if len(attrs) == 0 {
return nil
}

return attrs
}
Loading
Loading