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
41 changes: 37 additions & 4 deletions cli/src/cmd/app/commands/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type statusReport struct {
PID int `json:"pid,omitempty"`
DashboardURL string `json:"dashboardUrl,omitempty"`
StartTime time.Time `json:"startTime,omitempty"`
Uptime string `json:"uptime,omitempty"`
Services []runstate.ServiceState `json:"services,omitempty"`
}

Expand All @@ -41,8 +42,8 @@ func NewStatusCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Show whether azd app run is active",
Long: `Show whether azd app run is active, along with its PID, dashboard URL, and
running services.
Long: `Show whether azd app run is active, along with its PID, dashboard URL,
uptime, and running services.

Pass --watch to refresh the status on an interval until you press Ctrl+C, which
is handy for keeping an eye on services while they start. The global --json flag
Expand Down Expand Up @@ -180,13 +181,17 @@ func buildStatusReport(projectDir string) (statusReport, error) {
dashboardURL = fmt.Sprintf("http://localhost:%d", port)
}

return statusReport{
report := statusReport{
Running: true,
PID: st.PID,
DashboardURL: dashboardURL,
StartTime: st.StartTime,
Services: statusServices(projectDir, st.Services),
}, nil
}
if !st.StartTime.IsZero() {
report.Uptime = formatUptime(time.Since(st.StartTime))
}
return report, nil
}

func filterStatusReport(report statusReport, requested []string) (statusReport, error) {
Expand Down Expand Up @@ -278,6 +283,9 @@ func statusTextLines(report statusReport) []string {
if !report.StartTime.IsZero() {
lines = append(lines, fmt.Sprintf("Started: %s", report.StartTime.Format(time.RFC3339)))
}
if report.Uptime != "" {
lines = append(lines, fmt.Sprintf("Uptime: %s", report.Uptime))
}

if len(report.Services) == 0 {
lines = append(lines, "Services: none")
Expand All @@ -292,6 +300,31 @@ func statusTextLines(report statusReport) []string {
return lines
}

// formatUptime renders a duration as a compact human-readable string such as
// "45s", "3m12s", "1h04m", or "2d03h". Negative durations are clamped to zero.
// The largest two units are shown so the value stays short at any scale.
func formatUptime(d time.Duration) string {
if d < 0 {
d = 0
}
totalSeconds := int64(d / time.Second)
days := totalSeconds / 86400
hours := (totalSeconds % 86400) / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60

switch {
case days > 0:
return fmt.Sprintf("%dd%02dh", days, hours)
case hours > 0:
return fmt.Sprintf("%dh%02dm", hours, minutes)
case minutes > 0:
return fmt.Sprintf("%dm%02ds", minutes, seconds)
default:
return fmt.Sprintf("%ds", seconds)
}
}

func formatStatusService(svc runstate.ServiceState) string {
var endpoint string
if svc.URL != "" {
Expand Down
56 changes: 56 additions & 0 deletions cli/src/cmd/app/commands/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -132,12 +133,67 @@ func TestBuildStatusReportRunning(t *testing.T) {
assert.Contains(t, text, "Dashboard: http://localhost:40000")
assert.Contains(t, text, "Services:")
assert.Contains(t, text, " - api: http://localhost:8080")
assert.NotEmpty(t, report.Uptime, "expected uptime to be computed for a running app")

payload, err := json.Marshal(report)
require.NoError(t, err)
assert.Contains(t, string(payload), `"running":true`)
assert.Contains(t, string(payload), `"dashboardUrl":"http://localhost:40000"`)
assert.Contains(t, string(payload), `"name":"api"`)
assert.Contains(t, string(payload), `"uptime":`)
}

func TestStatusTextLinesUptime(t *testing.T) {
t.Run("uptime line is shown when set", func(t *testing.T) {
report := statusReport{
Running: true,
PID: 123,
StartTime: time.Now().Add(-90 * time.Second),
Uptime: "1m30s",
}
lines := statusTextLines(report)
assert.Contains(t, lines, "Uptime: 1m30s")

startedIdx, uptimeIdx := -1, -1
for i, line := range lines {
switch {
case strings.HasPrefix(line, "Started: "):
startedIdx = i
case strings.HasPrefix(line, "Uptime: "):
uptimeIdx = i
}
}
require.GreaterOrEqual(t, startedIdx, 0)
require.GreaterOrEqual(t, uptimeIdx, 0)
assert.Equal(t, startedIdx+1, uptimeIdx, "Uptime should follow Started")
})

t.Run("uptime line is omitted when empty", func(t *testing.T) {
report := statusReport{Running: true, PID: 123}
for _, line := range statusTextLines(report) {
assert.NotContains(t, line, "Uptime:")
}
})
}

func TestFormatUptime(t *testing.T) {
tests := []struct {
name string
in time.Duration
want string
}{
{"negative clamps to zero", -5 * time.Second, "0s"},
{"seconds only", 45 * time.Second, "45s"},
{"minutes and seconds", 3*time.Minute + 12*time.Second, "3m12s"},
{"pads seconds", 3*time.Minute + 2*time.Second, "3m02s"},
{"hours and minutes drop seconds", time.Hour + 4*time.Minute + 9*time.Second, "1h04m"},
{"days and hours", 2*24*time.Hour + 3*time.Hour + 30*time.Minute, "2d03h"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, formatUptime(tt.in))
})
}
}

func TestFilterStatusReport(t *testing.T) {
Expand Down
Loading