Skip to content
This repository was archived by the owner on Jun 23, 2026. It is now read-only.
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
129 changes: 129 additions & 0 deletions cmd_fleet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"fmt"
"io"
"net/url"
"time"

"github.com/spf13/pflag"
)

var (
flagsFleet *pflag.FlagSet
flagFleetWindow time.Duration
)

const fleetDataset = "Default.Observe Agent/Events"

const opalFleetStatus = `filter kind = "AgentLifecycleEvent" | make_col host:string(identifiers["host.name"]), env:string(identifiers["observe.agent.environment"]), version:string(facets["observe.agent.version"]), instance_id:string(identifiers["observe.agent.instance.id"]), data_obj:parse_json(data) | make_col auth_ok:bool(data_obj.authCheck.passed) | pick_col valid_from, host, env, version, auth_ok, instance_id | sort desc(valid_from)`

const opalFleetVersions = `filter kind = "AgentLifecycleEvent" | make_col host:string(identifiers["host.name"]), env:string(identifiers["observe.agent.environment"]), version:string(facets["observe.agent.version"]) | pick_col valid_from, host, env, version | sort asc(version), asc(host)`

const opalFleetAuth = `filter kind = "AgentLifecycleEvent" | make_col host:string(identifiers["host.name"]), env:string(identifiers["observe.agent.environment"]), version:string(facets["observe.agent.version"]), data_obj:parse_json(data) | make_col auth_ok:bool(data_obj.authCheck.passed), auth_code:int64(data_obj.authCheck.responseCode), auth_url:string(data_obj.authCheck.url) | pick_col valid_from, host, env, version, auth_ok, auth_code, auth_url | sort asc(auth_ok), desc(valid_from)`

func opalFleetHost(hostname string) string {
return fmt.Sprintf(
`filter kind = "AgentLifecycleEvent" | filter string(identifiers["host.name"]) = %q | make_col host:string(identifiers["host.name"]), env:string(identifiers["observe.agent.environment"]), version:string(facets["observe.agent.version"]), data_obj:parse_json(data) | make_col auth_ok:bool(data_obj.authCheck.passed), start_time:from_nanoseconds(int64(data_obj.agentStartTime)*1000000000) | pick_col valid_from, host, env, version, auth_ok, start_time | sort desc(valid_from)`,
hostname,
)
}

func init() {
flagsFleet = pflag.NewFlagSet("fleet", pflag.ContinueOnError)
flagsFleet.DurationVar(&flagFleetWindow, "window", 20*time.Minute, "time window for the query (e.g. 20m, 24h, 168h)")
RegisterCommand(&Command{
Name: "fleet",
Help: "Query fleet status of observe-agent instances from Default.Observe Agent/Events.",
Flags: flagsFleet,
Func: cmdFleet,
})
}

var (
ErrFleetUsage = ObserveError{Msg: "usage: observe fleet <status|host <hostname>|versions|auth> [--window <duration>]"}
)

func cmdFleet(fa FuncArgs) error {
// fa.args[0] is "fleet", fa.args[1] (if present) is the subcommand
if len(fa.args) < 2 {
return ErrFleetUsage
}

subcommand := fa.args[1]

var opalText string
var window time.Duration

if flagFleetWindow > 0 {
window = flagFleetWindow
} else {
window = 20 * time.Minute
}

switch subcommand {
case "status":
opalText = opalFleetStatus
case "host":
if len(fa.args) < 3 {
return ObserveError{Msg: "usage: observe fleet host <hostname> [--window <duration>]"}
}
hostname := fa.args[2]
opalText = opalFleetHost(hostname)
case "versions":
opalText = opalFleetVersions
case "auth":
opalText = opalFleetAuth
default:
return NewObserveError(nil, "unknown fleet subcommand %q; use status, host, versions, or auth", subcommand)
}

return runFleetQuery(fa, opalText, window)
}

func runFleetQuery(fa FuncArgs, opalText string, window time.Duration) error {
nowTime := time.Now().Truncate(time.Second)
toTime := nowTime.Add(-15 * time.Second).Truncate(time.Minute)
fromTime := toTime.Add(-window)

datasetPath := fleetDataset
noLinkify := false
req := V1ExportQueryRequest{
Query: OpalQuery{
OutputStage: "query",
Stages: []StageQuery{
{
Inputs: []StageQueryInput{
{
InputName: "_",
DatasetPath: &datasetPath,
},
},
StageID: "query",
Pipeline: opalText,
},
},
},
Presentation: &Presentation{
Linkify: &noLinkify,
},
}

tfmt := &CSVParsingColumnFormatter{
ColumnFormatter: ColumnFormatter{
Output: fa.op,
ColWidth: 64,
},
}
defer tfmt.Close()
var output io.Writer = tfmt

uri := fmt.Sprintf("/v1/meta/export/query?startTime=%s&endTime=%s",
url.QueryEscape(fromTime.Format(time.RFC3339)),
url.QueryEscape(toTime.Format(time.RFC3339)))

err, _ := RequestPOSTWithBodyOutput(fa.cfg, fa.op, fa.hc, uri, &req,
headers("Accept", "text/csv", "Authorization", fa.cfg.AuthHeader()),
output)
return err
}
87 changes: 87 additions & 0 deletions docs/fleet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# fleet

observe fleet status --window 20m
observe fleet host my-server.example.com --window 24h
observe fleet versions --window 168h
observe fleet auth --window 20m

The fleet command queries the `Default.Observe Agent/Events` resource dataset to
give you visibility into your deployed observe-agent instances. This dataset
receives heartbeat events approximately every 10 minutes, so a `--window` of
`20m` is sufficient to see every currently active agent.

## Subcommands

### status

observe fleet status [--window <duration>]

Shows the current agent inventory: for each recent heartbeat, the host name,
platform environment (windows, linux, macos, kubernetes, docker), agent version,
auth check result, and agent instance ID. Results are sorted newest first.

observe fleet status --window 20m

### host

observe fleet host <hostname> [--window <duration>]

Shows the event history for a single host over the given window. In addition to
the status columns, this shows the agent start time so you can track restarts.
Results are sorted newest first.

observe fleet host my-server.example.com --window 24h

### versions

observe fleet versions [--window <duration>]

Shows the version distribution across your fleet, sorted by version and then
host name. Use this to identify hosts that are running outdated agent versions.

observe fleet versions --window 168h

### auth

observe fleet auth [--window <duration>]

Shows auth check status from all agents. Failures are sorted first so they are
easy to spot. Each row includes the HTTP response code and the auth URL that was
checked, making it straightforward to diagnose authentication problems.

observe fleet auth --window 20m

## Time Window

The `--window` flag accepts Go duration strings such as `20m`, `1h`, `24h`, and
`168h` (one week). The window is anchored at the current time and extends
backward by the specified duration.

For current agent inventory, `--window 20m` is recommended because agents send
heartbeats every 10 minutes. For historical analysis, use longer windows such
as `--window 24h` or `--window 168h`.

## Dataset

All subcommands query `Default.Observe Agent/Events`, which is a resource
dataset in your Observe workspace. This dataset stores `AgentLifecycleEvent`
records containing host name, environment, agent version, instance ID, agent
start time, and authentication check results.

## Examples

Show all currently active agents:

observe fleet status --window 20m

Show the last 24 hours of events for a specific host:

observe fleet host prod-web-01.example.com --window 24h

Show version distribution across the whole fleet for the past week:

observe fleet versions --window 168h

Find agents with auth failures in the last 20 minutes:

observe fleet auth --window 20m