diff --git a/agent/unit_state_test.go b/agent/unit_state_test.go index f90115ea0..7c0697c29 100644 --- a/agent/unit_state_test.go +++ b/agent/unit_state_test.go @@ -780,7 +780,7 @@ func TestMarshalJSON(t *testing.T) { if err != nil { t.Fatalf("unexpected error marshalling: %v", err) } - want = `{"Cache":{"bar.service":{"LoadState":"","ActiveState":"inactive","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"bar.service"},"foo.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"foo.service"}},"ToPublish":{"woof.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"woof.service"}}}` + want = `{"Cache":{"bar.service":{"LoadState":"","ActiveState":"inactive","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"bar.service","ActiveEnterTimestamp":0},"foo.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"foo.service","ActiveEnterTimestamp":0}},"ToPublish":{"woof.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"woof.service","ActiveEnterTimestamp":0}}}` if string(got) != want { t.Fatalf("Bad JSON representation: got\n%s\n\nwant\n%s", string(got), want) } diff --git a/fleetctl/list_units.go b/fleetctl/list_units.go index 28f682ac3..1ca0cfe11 100644 --- a/fleetctl/list_units.go +++ b/fleetctl/list_units.go @@ -18,13 +18,14 @@ import ( "fmt" "sort" "strings" + "time" "github.com/coreos/fleet/machine" "github.com/coreos/fleet/schema" ) const ( - defaultListUnitsFields = "unit,machine,active,sub" + defaultListUnitsFields = "unit,machine,active,sub,uptime" ) var ( @@ -90,6 +91,14 @@ Or, choose the columns to display: } return us.Hash }, + "uptime": func(us *schema.UnitState, full bool) string { + if us == nil || us.SystemdActiveState != "active" { + return "-" + } + tm := time.Unix(0, int64(us.SystemdActiveEnterTimestamp)*1000) + duration := time.Now().Sub(tm) + return fmt.Sprintf("%s, Since %ss", tm.Format("2006-01-02 03:04:05 PM"), strings.Split(duration.String(), ".")[0]) + }, } ) diff --git a/registry/unit_state.go b/registry/unit_state.go index 5b67d1919..8707623aa 100644 --- a/registry/unit_state.go +++ b/registry/unit_state.go @@ -190,11 +190,12 @@ func (r *EtcdRegistry) RemoveUnitState(jobName string) error { } type unitStateModel struct { - LoadState string `json:"loadState"` - ActiveState string `json:"activeState"` - SubState string `json:"subState"` - MachineState *machine.MachineState `json:"machineState"` - UnitHash string `json:"unitHash"` + LoadState string `json:"loadState"` + ActiveState string `json:"activeState"` + SubState string `json:"subState"` + MachineState *machine.MachineState `json:"machineState"` + UnitHash string `json:"unitHash"` + ActiveEnterTimestamp uint64 `json:"ActiveEnterTimestamp"` } func modelToUnitState(usm *unitStateModel, name string) *unit.UnitState { @@ -203,11 +204,12 @@ func modelToUnitState(usm *unitStateModel, name string) *unit.UnitState { } us := unit.UnitState{ - LoadState: usm.LoadState, - ActiveState: usm.ActiveState, - SubState: usm.SubState, - UnitHash: usm.UnitHash, - UnitName: name, + LoadState: usm.LoadState, + ActiveState: usm.ActiveState, + SubState: usm.SubState, + UnitHash: usm.UnitHash, + UnitName: name, + ActiveEnterTimestamp: usm.ActiveEnterTimestamp, } if usm.MachineState != nil { @@ -229,10 +231,11 @@ func unitStateToModel(us *unit.UnitState) *unitStateModel { //} usm := unitStateModel{ - LoadState: us.LoadState, - ActiveState: us.ActiveState, - SubState: us.SubState, - UnitHash: us.UnitHash, + LoadState: us.LoadState, + ActiveState: us.ActiveState, + SubState: us.SubState, + UnitHash: us.UnitHash, + ActiveEnterTimestamp: us.ActiveEnterTimestamp, } if us.MachineID != "" { diff --git a/registry/unit_state_test.go b/registry/unit_state_test.go index 6c22c6e4a..683376ac5 100644 --- a/registry/unit_state_test.go +++ b/registry/unit_state_test.go @@ -101,7 +101,7 @@ func TestSaveUnitState(t *testing.T) { r := &EtcdRegistry{kAPI: e, keyPrefix: "/fleet/"} j := "foo.service" mID := "mymachine" - us := unit.NewUnitState("abc", "def", "ghi", mID) + us := unit.NewUnitState("abc", "def", "ghi", mID, 1234567890) // Saving nil unit state should fail r.SaveUnitState(j, nil, time.Second) @@ -123,7 +123,7 @@ func TestSaveUnitState(t *testing.T) { us.UnitHash = "quickbrownfox" r.SaveUnitState(j, us, time.Second) - json := `{"loadState":"abc","activeState":"def","subState":"ghi","machineState":{"ID":"mymachine","PublicIP":"","Metadata":null,"Version":""},"unitHash":"quickbrownfox"}` + json := `{"loadState":"abc","activeState":"def","subState":"ghi","machineState":{"ID":"mymachine","PublicIP":"","Metadata":null,"Version":""},"unitHash":"quickbrownfox","ActiveEnterTimestamp":1234567890}` p1 := "/fleet/state/foo.service" p2 := "/fleet/states/foo.service/mymachine" want := []action{ @@ -198,54 +198,60 @@ func TestUnitStateToModel(t *testing.T) { // Unit state with no hash and no machineID is OK // See https://github.com/coreos/fleet/issues/720 in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 0, }, want: &unitStateModel{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineState: nil, - UnitHash: "", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineState: nil, + UnitHash: "", + ActiveEnterTimestamp: 0, }, }, { // Unit state with hash but no machineID is OK in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "heh", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "heh", + UnitName: "name", + ActiveEnterTimestamp: 1234567890, }, want: &unitStateModel{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineState: nil, - UnitHash: "heh", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineState: nil, + UnitHash: "heh", + ActiveEnterTimestamp: 1234567890, }, }, { in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "woof", - UnitHash: "miaow", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "woof", + UnitHash: "miaow", + UnitName: "name", + ActiveEnterTimestamp: 54321, }, want: &unitStateModel{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineState: &machine.MachineState{ID: "woof"}, - UnitHash: "miaow", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineState: &machine.MachineState{ID: "woof"}, + UnitHash: "miaow", + ActiveEnterTimestamp: 54321, }, }, } { @@ -266,25 +272,27 @@ func TestModelToUnitState(t *testing.T) { want: nil, }, { - in: &unitStateModel{"foo", "bar", "baz", nil, ""}, + in: &unitStateModel{"foo", "bar", "baz", nil, "", 1234567890}, want: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 1234567890, }, }, { - in: &unitStateModel{"z", "x", "y", &machine.MachineState{ID: "abcd"}, ""}, + in: &unitStateModel{"z", "x", "y", &machine.MachineState{ID: "abcd"}, "", 987654321}, want: &unit.UnitState{ - LoadState: "z", - ActiveState: "x", - SubState: "y", - MachineID: "abcd", - UnitHash: "", - UnitName: "name", + LoadState: "z", + ActiveState: "x", + SubState: "y", + MachineID: "abcd", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 987654321, }, }, } { diff --git a/schema/mapper.go b/schema/mapper.go index e4dd05c38..363bdc380 100644 --- a/schema/mapper.go +++ b/schema/mapper.go @@ -115,12 +115,13 @@ func MapUnitStatesToSchemaUnitStates(entities []*unit.UnitState) []*UnitState { func MapUnitStateToSchemaUnitState(entity *unit.UnitState) *UnitState { us := UnitState{ - Name: entity.UnitName, - Hash: entity.UnitHash, - MachineID: entity.MachineID, - SystemdLoadState: entity.LoadState, - SystemdActiveState: entity.ActiveState, - SystemdSubState: entity.SubState, + Name: entity.UnitName, + Hash: entity.UnitHash, + MachineID: entity.MachineID, + SystemdLoadState: entity.LoadState, + SystemdActiveState: entity.ActiveState, + SystemdSubState: entity.SubState, + SystemdActiveEnterTimestamp: entity.ActiveEnterTimestamp, } return &us @@ -130,12 +131,13 @@ func MapSchemaUnitStatesToUnitStates(entities []*UnitState) []*unit.UnitState { us := make([]*unit.UnitState, len(entities)) for i, e := range entities { us[i] = &unit.UnitState{ - UnitName: e.Name, - UnitHash: e.Hash, - MachineID: e.MachineID, - LoadState: e.SystemdLoadState, - ActiveState: e.SystemdActiveState, - SubState: e.SystemdSubState, + UnitName: e.Name, + UnitHash: e.Hash, + MachineID: e.MachineID, + LoadState: e.SystemdLoadState, + ActiveState: e.SystemdActiveState, + SubState: e.SystemdSubState, + ActiveEnterTimestamp: e.SystemdActiveEnterTimestamp, } } diff --git a/schema/v1-gen.go b/schema/v1-gen.go index 24a215e3d..f078b981a 100644 --- a/schema/v1-gen.go +++ b/schema/v1-gen.go @@ -141,6 +141,8 @@ type UnitState struct { SystemdLoadState string `json:"systemdLoadState,omitempty"` SystemdSubState string `json:"systemdSubState,omitempty"` + + SystemdActiveEnterTimestamp uint64 `json:"systemdActiveEnterTimestamp,omitempty"` } type UnitStatePage struct { diff --git a/systemd/manager.go b/systemd/manager.go index d01b7b33a..a564b4666 100644 --- a/systemd/manager.go +++ b/systemd/manager.go @@ -238,6 +238,17 @@ func (m *systemdUnitManager) GetUnitStates(filter pkg.Set) (map[string]*unit.Uni states[name] = us } + // add Active enter time to UnitState + for name, us := range states { + prop, err := m.systemd.GetUnitProperty(name, "ActiveEnterTimestamp") + if err != nil { + return nil, err + } + + us.ActiveEnterTimestamp = prop.Value.Value().(uint64) + states[name] = us + } + return states, nil } diff --git a/unit/fake.go b/unit/fake.go index bc82352e0..fd450e7ad 100644 --- a/unit/fake.go +++ b/unit/fake.go @@ -83,7 +83,7 @@ func (fum *FakeUnitManager) GetUnitStates(filter pkg.Set) (map[string]*UnitState states := make(map[string]*UnitState) for _, name := range filter.Values() { if _, ok := fum.u[name]; ok { - states[name] = &UnitState{"loaded", "active", "running", "", "", name} + states[name] = &UnitState{"loaded", "active", "running", "", "", name, 0} } } diff --git a/unit/fake_test.go b/unit/fake_test.go index 3b616f2e8..2df16cdab 100644 --- a/unit/fake_test.go +++ b/unit/fake_test.go @@ -60,7 +60,7 @@ func TestFakeUnitManagerLoadUnload(t *testing.T) { t.Fatalf("Expected non-nil UnitState") } - eus := NewUnitState("loaded", "active", "running", "") + eus := NewUnitState("loaded", "active", "running", "", 0) if !reflect.DeepEqual(*us, *eus) { t.Fatalf("Expected UnitState %v, got %v", eus, *us) } diff --git a/unit/generator_test.go b/unit/generator_test.go index bd3fe807c..b99ccfd8c 100644 --- a/unit/generator_test.go +++ b/unit/generator_test.go @@ -49,7 +49,7 @@ func TestUnitStateGeneratorSubscribeLifecycle(t *testing.T) { // subscribed to foo.service so we should get a heartbeat expect := []UnitStateHeartbeat{ - UnitStateHeartbeat{Name: "foo.service", State: &UnitState{"loaded", "active", "running", "", "", "foo.service"}}, + UnitStateHeartbeat{Name: "foo.service", State: &UnitState{"loaded", "active", "running", "", "", "foo.service", 0}}, } assertGenerateUnitStateHeartbeats(t, um, gen, expect) diff --git a/unit/unit.go b/unit/unit.go index 4f9be39bc..6b1109d24 100644 --- a/unit/unit.go +++ b/unit/unit.go @@ -171,20 +171,22 @@ func (h *Hash) Empty() bool { // UnitState encodes the current state of a unit loaded into a fleet agent type UnitState struct { - LoadState string - ActiveState string - SubState string - MachineID string - UnitHash string - UnitName string + LoadState string + ActiveState string + SubState string + MachineID string + UnitHash string + UnitName string + ActiveEnterTimestamp uint64 } -func NewUnitState(loadState, activeState, subState, mID string) *UnitState { +func NewUnitState(loadState, activeState, subState, mID string, activeEnterTimestamp uint64) *UnitState { return &UnitState{ - LoadState: loadState, - ActiveState: activeState, - SubState: subState, - MachineID: mID, + LoadState: loadState, + ActiveState: activeState, + SubState: subState, + MachineID: mID, + ActiveEnterTimestamp: activeEnterTimestamp, } } diff --git a/unit/unit_test.go b/unit/unit_test.go index d3844676d..0a62e9dd2 100644 --- a/unit/unit_test.go +++ b/unit/unit_test.go @@ -93,15 +93,16 @@ func TestDefaultUnitType(t *testing.T) { func TestNewUnitState(t *testing.T) { want := &UnitState{ - LoadState: "ls", - ActiveState: "as", - SubState: "ss", - MachineID: "id", + LoadState: "ls", + ActiveState: "as", + SubState: "ss", + MachineID: "id", + ActiveEnterTimestamp: 1234567890, } - got := NewUnitState("ls", "as", "ss", "id") + got := NewUnitState("ls", "as", "ss", "id", 1234567890) if !reflect.DeepEqual(got, want) { - t.Fatalf("NewUnitState did not create a correct UnitState: got %s, want %s", got, want) + t.Fatalf("NewUnitState did not create a correct UnitState: got %v, want %v", got, want) } }