Skip to content

Commit 0c69d7e

Browse files
Merge pull request #34 from diveshpatil9104/admin-status
implement GET /api/v1/admin/status endpoint for system health monito…
2 parents b41de74 + 529bb89 commit 0c69d7e

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

handlers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,27 @@ func buildFeed(vehicles []*VehicleState) *gtfs.FeedMessage {
157157
return feed
158158
}
159159

160+
type adminStatusResponse struct {
161+
Status string `json:"status"`
162+
UptimeSeconds int64 `json:"uptime_seconds"`
163+
ActiveVehicles int `json:"active_vehicles"`
164+
TotalVehiclesTracked int `json:"total_vehicles_tracked"`
165+
LastUpdate *time.Time `json:"last_update,omitempty"`
166+
}
167+
168+
func handleAdminStatus(tracker *Tracker, startTime time.Time) http.HandlerFunc {
169+
return func(w http.ResponseWriter, r *http.Request) {
170+
ts := tracker.Status()
171+
writeJSON(w, http.StatusOK, adminStatusResponse{
172+
Status: "ok",
173+
UptimeSeconds: int64(time.Since(startTime).Seconds()),
174+
ActiveVehicles: ts.ActiveVehicles,
175+
TotalVehiclesTracked: ts.TotalVehiclesTracked,
176+
LastUpdate: ts.LastUpdate,
177+
})
178+
}
179+
}
180+
160181
func writeJSON(w http.ResponseWriter, status int, v any) {
161182
w.Header().Set("Content-Type", "application/json")
162183
w.WriteHeader(status)

handlers_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,87 @@ func TestHandlePostLocation_Validation(t *testing.T) {
223223
}
224224
}
225225

226+
func TestHandleAdminStatus_Empty(t *testing.T) {
227+
tracker := NewTracker(5 * time.Minute)
228+
defer tracker.Stop()
229+
230+
handler := handleAdminStatus(tracker, time.Now())
231+
req := httptest.NewRequest("GET", "/api/v1/admin/status", nil)
232+
w := httptest.NewRecorder()
233+
handler(w, req)
234+
235+
assert.Equal(t, http.StatusOK, w.Code)
236+
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
237+
238+
var resp adminStatusResponse
239+
err := json.NewDecoder(w.Body).Decode(&resp)
240+
require.NoError(t, err)
241+
242+
assert.Equal(t, "ok", resp.Status)
243+
assert.Equal(t, 0, resp.ActiveVehicles)
244+
assert.Equal(t, 0, resp.TotalVehiclesTracked)
245+
assert.Nil(t, resp.LastUpdate)
246+
}
247+
248+
func TestHandleAdminStatus_WithVehicles(t *testing.T) {
249+
tracker := NewTracker(5 * time.Minute)
250+
defer tracker.Stop()
251+
252+
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 1, Longitude: 2, Timestamp: 100})
253+
tracker.Update(&LocationReport{VehicleID: "bus-2", Latitude: 3, Longitude: 4, Timestamp: 200})
254+
255+
handler := handleAdminStatus(tracker, time.Now())
256+
req := httptest.NewRequest("GET", "/api/v1/admin/status", nil)
257+
w := httptest.NewRecorder()
258+
handler(w, req)
259+
260+
assert.Equal(t, http.StatusOK, w.Code)
261+
262+
var resp adminStatusResponse
263+
err := json.NewDecoder(w.Body).Decode(&resp)
264+
require.NoError(t, err)
265+
266+
assert.Equal(t, "ok", resp.Status)
267+
assert.Equal(t, 2, resp.ActiveVehicles)
268+
assert.Equal(t, 2, resp.TotalVehiclesTracked)
269+
require.NotNil(t, resp.LastUpdate)
270+
assert.False(t, resp.LastUpdate.IsZero())
271+
}
272+
273+
func TestHandleAdminStatus_Uptime(t *testing.T) {
274+
tracker := NewTracker(5 * time.Minute)
275+
defer tracker.Stop()
276+
277+
startTime := time.Now().Add(-10 * time.Second)
278+
handler := handleAdminStatus(tracker, startTime)
279+
req := httptest.NewRequest("GET", "/api/v1/admin/status", nil)
280+
w := httptest.NewRecorder()
281+
handler(w, req)
282+
283+
var resp adminStatusResponse
284+
err := json.NewDecoder(w.Body).Decode(&resp)
285+
require.NoError(t, err)
286+
287+
assert.GreaterOrEqual(t, resp.UptimeSeconds, int64(10))
288+
}
289+
290+
func TestHandleAdminStatus_ZeroVehiclesNotNull(t *testing.T) {
291+
tracker := NewTracker(5 * time.Minute)
292+
defer tracker.Stop()
293+
294+
handler := handleAdminStatus(tracker, time.Now())
295+
req := httptest.NewRequest("GET", "/api/v1/admin/status", nil)
296+
w := httptest.NewRecorder()
297+
handler(w, req)
298+
299+
var raw map[string]any
300+
err := json.NewDecoder(w.Body).Decode(&raw)
301+
require.NoError(t, err)
302+
303+
assert.Equal(t, float64(0), raw["active_vehicles"])
304+
assert.Equal(t, float64(0), raw["total_vehicles_tracked"])
305+
}
306+
226307
type mockStore struct {
227308
err error
228309
saved bool

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ func main() {
4747
log.Printf("seeded tracker with %d active vehicles", len(recentLocations))
4848
}
4949

50+
startTime := time.Now()
51+
5052
mux := http.NewServeMux()
5153
mux.HandleFunc("POST /api/v1/locations", handlePostLocation(store, tracker))
5254
mux.HandleFunc("GET /gtfs-rt/vehicle-positions", handleGetFeed(tracker))
55+
mux.HandleFunc("GET /api/v1/admin/status", handleAdminStatus(tracker, startTime))
5356
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
5457
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
5558
})

tracker.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,35 @@ func (t *Tracker) ActiveVehicles() []*VehicleState {
9191
return active
9292
}
9393

94+
// TrackerStatus holds aggregate statistics about tracked vehicles.
95+
type TrackerStatus struct {
96+
ActiveVehicles int
97+
TotalVehiclesTracked int
98+
LastUpdate *time.Time
99+
}
100+
101+
// Status returns aggregate statistics with a single lock acquisition.
102+
func (t *Tracker) Status() TrackerStatus {
103+
t.mu.RLock()
104+
defer t.mu.RUnlock()
105+
cutoff := time.Now().Add(-t.maxAge)
106+
var s TrackerStatus
107+
s.TotalVehiclesTracked = len(t.vehicles)
108+
var latest time.Time
109+
for _, v := range t.vehicles {
110+
if v.UpdatedAt.After(cutoff) {
111+
s.ActiveVehicles++
112+
}
113+
if v.UpdatedAt.After(latest) {
114+
latest = v.UpdatedAt
115+
}
116+
}
117+
if !latest.IsZero() {
118+
s.LastUpdate = &latest
119+
}
120+
return s
121+
}
122+
94123
// cleanup removes old entries from the tracker to prevent unbounded memory growth.
95124
func (t *Tracker) cleanup() {
96125
t.mu.Lock()

tracker_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,50 @@ func TestTracker_StopIsIdempotent(t *testing.T) {
148148
assert.NotPanics(t, func() { tracker.Stop() })
149149
}
150150

151+
func TestTracker_Status_Empty(t *testing.T) {
152+
tracker := NewTracker(5 * time.Minute)
153+
defer tracker.Stop()
154+
155+
s := tracker.Status()
156+
157+
assert.Equal(t, 0, s.ActiveVehicles)
158+
assert.Equal(t, 0, s.TotalVehiclesTracked)
159+
assert.Nil(t, s.LastUpdate)
160+
}
161+
162+
func TestTracker_Status_WithActiveVehicles(t *testing.T) {
163+
tracker := NewTracker(5 * time.Minute)
164+
defer tracker.Stop()
165+
166+
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 1, Longitude: 2, Timestamp: 100})
167+
tracker.Update(&LocationReport{VehicleID: "bus-2", Latitude: 3, Longitude: 4, Timestamp: 200})
168+
169+
s := tracker.Status()
170+
171+
assert.Equal(t, 2, s.ActiveVehicles)
172+
assert.Equal(t, 2, s.TotalVehiclesTracked)
173+
require.NotNil(t, s.LastUpdate)
174+
assert.False(t, s.LastUpdate.IsZero())
175+
}
176+
177+
func TestTracker_Status_WithStaleVehicles(t *testing.T) {
178+
tracker := NewTracker(5 * time.Minute)
179+
defer tracker.Stop()
180+
181+
tracker.Update(&LocationReport{VehicleID: "old-bus", Latitude: 1, Longitude: 2, Timestamp: 100})
182+
tracker.mu.Lock()
183+
tracker.vehicles["old-bus"].UpdatedAt = time.Now().Add(-10 * time.Minute)
184+
tracker.mu.Unlock()
185+
186+
tracker.Update(&LocationReport{VehicleID: "new-bus", Latitude: 3, Longitude: 4, Timestamp: 200})
187+
188+
s := tracker.Status()
189+
190+
assert.Equal(t, 1, s.ActiveVehicles)
191+
assert.Equal(t, 2, s.TotalVehiclesTracked)
192+
require.NotNil(t, s.LastUpdate)
193+
}
194+
151195
func TestTracker_CleanupPreservesFreshVehicles(t *testing.T) {
152196
maxAge := 50 * time.Millisecond
153197
tracker := NewTracker(maxAge)

0 commit comments

Comments
 (0)