Skip to content

Commit

Permalink
format API responses as JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmasek committed Jan 11, 2025
1 parent fc6d5af commit 71ffb58
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 27 deletions.
4 changes: 4 additions & 0 deletions README-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This documents describes some internals, implementation details, and tries it's

You should start with the main [README](README.md).

TODO: json API responses
TODO: deploy and try how it works
TODO:

## 🚧 Feature list:
- 🟢 heartbeat listener
- 🟢 HTTP server
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,23 @@ Send a Heartbeat:
curl -X POST http://localhost:8088/services/my-service-name/beat
```
Response:
```sh
my-service-name @ 2025-01-11T12:34:56Z
```json
{
"service_id": "my-service-name",
"timestamp": "2025-01-11T17:20:09Z"
}
```

Get Service Status:
```sh
curl -X GET http://localhost:8088/services/my-service-name/status
```
Response:
```sh
my-service-name @ 2025-01-11T12:34:56Z
```json
{
"service_id": "my-service-name",
"timestamp": "2025-01-11T17:20:09Z"
}
```


Expand Down
50 changes: 37 additions & 13 deletions monitor/heartbeat_listener.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package monitor

import (
"fmt"
"encoding/json"
"log"
"net/http"
"time"
Expand All @@ -11,57 +11,81 @@ import (
"github.com/davidmasek/beacon/storage"
)

type HeartbeatResponse struct {
ServiceId string `json:"service_id"`
Timestamp string `json:"timestamp"`
}

type StatusResponse struct {
ServiceId string `json:"service_id"`
Timestamp string `json:"timestamp,omitempty"`
Message string `json:"message,omitempty"`
}

func RegisterHeartbeatHandlers(db storage.Storage, mux *http.ServeMux) {
mux.HandleFunc("/services/{service_id}/beat", handleBeat(db))
mux.HandleFunc("/services/{service_id}/status", handleStatus(db))
}

func handleBeat(db storage.Storage) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
serviceID := r.PathValue("service_id")
if serviceID == "" {
serviceId := r.PathValue("service_id")
if serviceId == "" {
http.Error(w, "Missing service_id", http.StatusBadRequest)
return
}
now := time.Now()
// Log the heartbeat to the database
nowStr, err := db.RecordHeartbeat(serviceID, now)
nowStr, err := db.RecordHeartbeat(serviceId, now)
if err != nil {
log.Println("[ERROR]", err)
http.Error(w, "Failed to log heartbeat", http.StatusInternalServerError)
return
}

response := HeartbeatResponse{
ServiceId: serviceId,
Timestamp: nowStr,
}

// Respond to the client
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s @ %s\n", serviceID, nowStr)
json.NewEncoder(w).Encode(response)
}
}

func handleStatus(db storage.Storage) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
serviceID := r.PathValue("service_id")
if serviceID == "" {
serviceId := r.PathValue("service_id")
if serviceId == "" {
http.Error(w, "Missing service_id", http.StatusBadRequest)
return
}

// Query the database for the latest heartbeat
timestamps, err := db.GetLatestHeartbeats(serviceID, 1)
timestamps, err := db.GetLatestHeartbeats(serviceId, 1)
if err != nil {
log.Println("[ERROR]", err)
http.Error(w, "Failed to query database", http.StatusInternalServerError)
return
}
var response StatusResponse
if len(timestamps) == 0 {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s @ never\n", serviceID)
return
response = StatusResponse{
ServiceId: serviceId,
Message: "never",
}
} else {
response = StatusResponse{
ServiceId: serviceId,
Timestamp: timestamps[0].UTC().Format(storage.TIME_FORMAT),
}
}
timestamp := timestamps[0]

// Respond to the client
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s @ %s\n", serviceID, timestamp.UTC().Format(storage.TIME_FORMAT))
json.NewEncoder(w).Encode(response)
}
}
26 changes: 16 additions & 10 deletions tests/e2e_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package tests

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"

"github.com/davidmasek/beacon/conf"
"github.com/davidmasek/beacon/handlers"
"github.com/davidmasek/beacon/monitor"
"github.com/davidmasek/beacon/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -28,7 +29,7 @@ func TestEndToEndHeartbeat(t *testing.T) {
db := setupDB(t)
defer db.Close()

service_name := "heartbeat-monitor"
serviceName := "heartbeat-monitor"

config := conf.NewConfig()
// shouldn't be fixed, but at least it's different than the default
Expand All @@ -44,20 +45,25 @@ func TestEndToEndHeartbeat(t *testing.T) {
time.Sleep(100 * time.Millisecond)

t.Log("Record heartbeat")
input := Post(fmt.Sprintf("/services/%s/beat", service_name), t, serverPort)
assert.Contains(t, input, service_name)
timestampIn := strings.Split(input, " ")[2]
var heartbeatResponse monitor.HeartbeatResponse
input := Post(fmt.Sprintf("/services/%s/beat", serviceName), t, serverPort)
err = json.Unmarshal([]byte(input), &heartbeatResponse)
require.NoError(t, err, "Failed to parse JSON response")
assert.Equal(t, serviceName, heartbeatResponse.ServiceId, "Service ID does not match")
timestampIn := heartbeatResponse.Timestamp

t.Log("Retrieve heartbeat status")
output := Get(fmt.Sprintf("/services/%s/status", service_name), t, serverPort)
assert.Contains(t, output, service_name)
timestampOut := strings.Split(output, " ")[2]
assert.Equal(t, timestampIn, timestampOut)
output := Get(fmt.Sprintf("/services/%s/status", serviceName), t, serverPort)
var statusResponse monitor.StatusResponse
err = json.Unmarshal([]byte(output), &statusResponse)
require.NoError(t, err, "Failed to parse JSON response")
assert.Equal(t, serviceName, statusResponse.ServiceId, "Service ID does not match")
assert.Equal(t, timestampIn, statusResponse.Timestamp, "Timestamps do not match")

t.Log("Check web UI")
html := Get("/", t, serverPort)
assert.Contains(t, html, "<html")
assert.Contains(t, html, service_name)
assert.Contains(t, html, serviceName)
}

// TODO: could replace with resty ?
Expand Down

0 comments on commit 71ffb58

Please sign in to comment.