Skip to content

Commit

Permalink
Add timestamp option (recent time reach the status) to service status (
Browse files Browse the repository at this point in the history
…#429)

* Add timestamp option (recent time reach the status) to service status

* address comments

* fix typo
  • Loading branch information
sfc-gh-pchu authored May 6, 2024
1 parent bc8a10e commit 62be27c
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 116 deletions.
37 changes: 32 additions & 5 deletions services/service/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io"
"sort"
"strings"
"time"

"github.com/google/subcommands"

Expand Down Expand Up @@ -187,18 +188,39 @@ func (a *actionCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interf
}

type statusCmd struct {
systemType string
systemType string
displayTimestamp bool
}

func (*statusCmd) Name() string { return "status" }
func (*statusCmd) Synopsis() string { return "retrieve service status" }
func (*statusCmd) Usage() string {
return `status [--system-type <type>] <service>
return `status [--system-type <type>] [-t] <service>
return the status of the specified service
`
}
func (s *statusCmd) SetFlags(f *flag.FlagSet) {
systemTypeFlag(f, &s.systemType)
f.BoolVar(&s.displayTimestamp, "t", false, "display recent timestamp that the service reach the current status")
}

func formatTimeAgo(t time.Time) string {
duration := time.Since(t)
secondsAgo := int(duration.Seconds())

switch {
case secondsAgo < 60:
return fmt.Sprintf("%d sec ago", secondsAgo)
case secondsAgo < 3600:
minutesAgo := secondsAgo / 60
return fmt.Sprintf("%d min ago", minutesAgo)
case secondsAgo < 86400:
hoursAgo := secondsAgo / 3600
return fmt.Sprintf("%d hour ago", hoursAgo)
default:
daysAgo := secondsAgo / 86400
return fmt.Sprintf("%d day ago", daysAgo)
}
}

func (s *statusCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
Expand All @@ -219,8 +241,9 @@ func (s *statusCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interf
}

req := &pb.StatusRequest{
SystemType: system,
ServiceName: serviceName,
SystemType: system,
ServiceName: serviceName,
DisplayTimestamp: s.displayTimestamp,
}
c := pb.NewServiceClientProxy(state.Conn)

Expand All @@ -244,8 +267,12 @@ func (s *statusCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interf
var lastErr error
for resp := range respChan {
out := state.Out[resp.Index]
system, status := resp.Resp.GetSystemType(), resp.Resp.GetServiceStatus().GetStatus()
system, status, timestamp := resp.Resp.GetSystemType(), resp.Resp.GetServiceStatus().GetStatus(), resp.Resp.GetServiceStatus().GetRecentTimestampReachCurrentStatus()
output := fmt.Sprintf("[%s] %s : %s", systemTypeString(system), serviceName, statusString(status))
if s.displayTimestamp {
output = fmt.Sprintf("[%s] %s : %s since %s; %s", systemTypeString(system), serviceName, statusString(status), timestamp.AsTime().Format("Mon 2006-01-02 15:04:05 MST"), formatTimeAgo(timestamp.AsTime()))
}

if resp.Error != nil {
lastErr = fmt.Errorf("target %s [%d] error: %w\n", resp.Target, resp.Index, resp.Error)
fmt.Fprint(state.Err[resp.Index], lastErr)
Expand Down
40 changes: 38 additions & 2 deletions services/service/server/server_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import (
"encoding/json"
"sort"
"strings"
"time"

"github.com/coreos/go-systemd/v22/dbus"
"github.com/go-logr/logr"
"go.opentelemetry.io/otel/attribute"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"

pb "github.com/Snowflake-Labs/sansshell/services/service"
"github.com/Snowflake-Labs/sansshell/telemetry/metrics"
Expand Down Expand Up @@ -116,6 +118,26 @@ func unitStateToStatus(u dbus.UnitStatus) pb.Status {
}
}

// if service status is running/stopped, get the recent timestamp that service reach the running/stopped status
func getTimestampReachCurrentStatus(s pb.Status, unitProperties map[string]interface{}) (*timestamppb.Timestamp, error) {
switch s {
case pb.Status_STATUS_RUNNING:
activeEnterTimestamp, ok := unitProperties["ActiveEnterTimestamp"].(uint64)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "Failed to convert ActiveEnterTimestamp %T to uint64", unitProperties["ActiveEnterTimestamp"])
}
return timestamppb.New(time.UnixMicro(int64(activeEnterTimestamp))), nil
case pb.Status_STATUS_STOPPED:
inactiveEnterTimestamp, ok := unitProperties["InactiveEnterTimestamp"].(uint64)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "Failed to convert InactiveEnterTimestamp %T to uint64", unitProperties["inactiveEnterTimestamp"])
}
return timestamppb.New(time.UnixMicro(int64(inactiveEnterTimestamp))), nil
default:
return nil, status.Errorf(codes.InvalidArgument, "Unknown status won't be able to get timestamp")
}
}

// a subset of dbus.Conn used to mock for testing
type systemdConnection interface {
GetUnitPropertiesContext(ctx context.Context, unit string) (map[string]interface{}, error)
Expand Down Expand Up @@ -244,17 +266,31 @@ func (s *server) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusR
logger.V(3).Info("failed to marshal properties: " + err.Error())
return nil, status.Errorf(codes.Internal, "failed to marshal unit properties to json")
}

unitState := dbus.UnitStatus{}
if errUnmarshal := json.Unmarshal(propertiesJson, &unitState); errUnmarshal != nil {
recorder.CounterOrLog(ctx, serviceStatusFailureCounter, 1, attribute.String("reason", "json_unmarshal_err"))
logger.V(3).Info("failed to unmarshal properties: " + errUnmarshal.Error())
return nil, status.Errorf(codes.Internal, "failed to unmarshal unit properties to json")
}

unitStatus := unitStateToStatus(unitState)
var recentTimestampReachCurrentStatus *timestamppb.Timestamp
if req.DisplayTimestamp {
timestampReachCurrentStatus, err := getTimestampReachCurrentStatus(unitStatus, properties)
if err != nil {
recorder.CounterOrLog(ctx, serviceStatusFailureCounter, 1, attribute.String("reason", "get_timestamp_reach_current_status_err"))
logger.V(3).Info("getTimestampReachCurrentStatus err:" + err.Error())
return nil, status.Errorf(codes.Internal, "failed to get recent timestmap reach current status %v", err.Error())
}
recentTimestampReachCurrentStatus = timestampReachCurrentStatus
}
return &pb.StatusReply{
SystemType: pb.SystemType_SYSTEM_TYPE_SYSTEMD,
ServiceStatus: &pb.ServiceStatus{
ServiceName: req.GetServiceName(),
Status: unitStateToStatus(unitState),
ServiceName: req.GetServiceName(),
Status: unitStatus,
RecentTimestampReachCurrentStatus: recentTimestampReachCurrentStatus,
},
}, nil
}
Expand Down
64 changes: 64 additions & 0 deletions services/service/server/server_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import (
"errors"
"strings"
"testing"
"time"

"github.com/coreos/go-systemd/v22/dbus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"

pb "github.com/Snowflake-Labs/sansshell/services/service"
"github.com/Snowflake-Labs/sansshell/testing/testutil"
Expand Down Expand Up @@ -254,6 +256,8 @@ func (g getConn) ReloadContext(ctx context.Context) error {
func (getConn) Close() {}

func TestStatus(t *testing.T) {
activeEnterTimestamp := uint64(1714766015324711)
inactiveEnterTimestamp := uint64(1714765926639587)
for _, tc := range []struct {
name string
conn systemdConnection
Expand Down Expand Up @@ -301,6 +305,16 @@ func TestStatus(t *testing.T) {
},
errFunc: testutil.FatalOnErr,
},
{
name: "unknown service get timestamp error",
conn: getConn([]map[string]interface{}{}),
req: &pb.StatusRequest{
ServiceName: "foo",
DisplayTimestamp: true,
},
want: nil,
errFunc: wantStatusErr(codes.Internal, "Unknown status won't be able to get timestamp"),
},
{
name: "accept with service suffix",
conn: getConn([]map[string]interface{}{
Expand Down Expand Up @@ -385,6 +399,56 @@ func TestStatus(t *testing.T) {
},
errFunc: testutil.FatalOnErr,
},
{
name: "running service get timestamp reach that state",
conn: getConn([]map[string]interface{}{
{
"Name": "foo.service",
"LoadState": loadStateLoaded,
"ActiveState": activeStateActive,
"SubState": substateRunning,
"ActiveEnterTimestamp": activeEnterTimestamp,
},
}),
req: &pb.StatusRequest{
ServiceName: "foo",
DisplayTimestamp: true,
},
want: &pb.StatusReply{
SystemType: pb.SystemType_SYSTEM_TYPE_SYSTEMD,
ServiceStatus: &pb.ServiceStatus{
ServiceName: "foo",
Status: pb.Status_STATUS_RUNNING,
RecentTimestampReachCurrentStatus: timestamppb.New(time.UnixMicro(int64(activeEnterTimestamp))),
},
},
errFunc: testutil.FatalOnErr,
},
{
name: "stopped service get timestamp reach that state",
conn: getConn([]map[string]interface{}{
{
"Name": "foo.service",
"LoadState": loadStateLoaded,
"ActiveState": activeStateActive,
"SubState": "dead",
"InactiveEnterTimestamp": inactiveEnterTimestamp,
},
}),
req: &pb.StatusRequest{
ServiceName: "foo",
DisplayTimestamp: true,
},
want: &pb.StatusReply{
SystemType: pb.SystemType_SYSTEM_TYPE_SYSTEMD,
ServiceStatus: &pb.ServiceStatus{
ServiceName: "foo",
Status: pb.Status_STATUS_STOPPED,
RecentTimestampReachCurrentStatus: timestamppb.New(time.UnixMicro(int64(inactiveEnterTimestamp))),
},
},
errFunc: testutil.FatalOnErr,
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading

0 comments on commit 62be27c

Please sign in to comment.