Skip to content

Commit

Permalink
Add server stats (#15)
Browse files Browse the repository at this point in the history
3. **Server Stats**: Shows real-time statistics about server usage:
   - Server Uptime
   - Download/Upload Counts (since start and last 24 hours)
   - HTTP Error Counts
   - Rate Limit Hit Count

4. **Rate Limit Stats**: Provides information about rate limiting, if applicable.
  • Loading branch information
dhcgn authored Jun 21, 2024
1 parent 19603ae commit 9f5dce9
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 214 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ This project provides a simple HTTP server that offers ephemeral storage for IoT
- Data is stored for a configurable duration before being deleted.
- The server can be run on a local network or in the cloud.

## Self-Hosted Info Website

The iot-ephemeral-value-store server includes a self-hosted info website that provides valuable information about the server's status, usage, and API. This website is automatically available when you run the server and can be accessed at the root URL (e.g., `http://127.0.0.1:8088/`).

### Features of the Info Website

1. **Getting Started Guide**: Provides example URLs for uploading, downloading, and deleting data, customized with a generated key pair for immediate use.

2. **Server Settings**: Displays important server configuration information, including:
- Software Version
- Software Build Time
- Data Retention Period

3. **Server Stats**: Shows real-time statistics about server usage:
- Server Uptime
- Download/Upload Counts (since start and last 24 hours)
- HTTP Error Counts
- Rate Limit Hit Count

4. **Rate Limit Stats**: Provides information about rate limiting, if applicable.

5. **API Usage Guide**: Offers a quick reference for using the server's API, including:
- Creating Key Pairs
- Uploading Data
- Downloading Data (JSON and Plain Text)
- Advanced Patch Usage

This self-hosted info website serves as a dashboard and quick-start guide, making it easier for users to understand and interact with the iot-ephemeral-value-store server.

## Diagrams

### Simple
Expand Down
2 changes: 2 additions & 0 deletions httphandler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"html"
"net/http"

"github.com/dhcgn/iot-ephemeral-value-store/stats"
"github.com/dhcgn/iot-ephemeral-value-store/storage"
)

type Config struct {
StorageInstance storage.Storage
StatsInstance *stats.Stats
}

func sanitizeInput(input string) string {
Expand Down
1 change: 1 addition & 0 deletions httphandler/deleteHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func (c Config) DeleteHandler(w http.ResponseWriter, r *http.Request) {

downloadKey, err := domain.DeriveDownloadKey(uploadKey)
if err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Error deriving download key", http.StatusInternalServerError)
return
}
Expand Down
9 changes: 9 additions & 0 deletions httphandler/downloadHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ func (c Config) PlainDownloadHandler(w http.ResponseWriter, r *http.Request) {

jsonData, err := c.StorageInstance.GetJSON(downloadKey)
if err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Invalid download key or database error", http.StatusNotFound)
return
}

// Parse the JSON data to retrieve the specific parameter
paramMap := make(map[string]interface{})
if err := json.Unmarshal(jsonData, &paramMap); err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Error decoding JSON", http.StatusInternalServerError)
return
}
Expand All @@ -36,15 +38,19 @@ func (c Config) PlainDownloadHandler(w http.ResponseWriter, r *http.Request) {
if m, ok := value.(map[string]interface{}); ok {
value, ok = m[key]
if !ok {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Parameter not found", http.StatusNotFound)
return
}
} else {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Invalid parameter path", http.StatusBadRequest)
return
}
}

c.StatsInstance.IncrementDownloads()

// Return the value as plain text
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, value)
Expand All @@ -56,10 +62,13 @@ func (c Config) DownloadHandler(w http.ResponseWriter, r *http.Request) {

jsonData, err := c.StorageInstance.GetJSON(downloadKey)
if err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Invalid download key or database error", http.StatusNotFound)
return
}

c.StatsInstance.IncrementDownloads()

// Set header and write the JSON data to the response writer
w.Header().Set("Content-Type", "application/json")
w.Write(jsonData)
Expand Down
1 change: 1 addition & 0 deletions httphandler/keyPairHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ func (c Config) KeyPairHandler(w http.ResponseWriter, r *http.Request) {
uploadKey := domain.GenerateRandomKey()
downloadKey, err := domain.DeriveDownloadKey(uploadKey)
if err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Error deriving download key", http.StatusInternalServerError)
return
}
Expand Down
3 changes: 3 additions & 0 deletions httphandler/uploadHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey,
// Derive download key
downloadKey, err := domain.DeriveDownloadKey(uploadKey)
if err != nil {
c.StatsInstance.IncrementHTTPErrors()
http.Error(w, "Error deriving download key", http.StatusInternalServerError)
return
}
Expand All @@ -51,6 +52,8 @@ func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey,
}
c.StorageInstance.Store(downloadKey, data)

c.StatsInstance.IncrementUploads()

// Construct and return response
constructAndReturnResponse(w, r, downloadKey, paramMap)
}
Expand Down
53 changes: 34 additions & 19 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import (
"text/template"
"time"

"github.com/dgraph-io/badger/v3"
"github.com/dhcgn/iot-ephemeral-value-store/domain"
"github.com/dhcgn/iot-ephemeral-value-store/httphandler"
"github.com/dhcgn/iot-ephemeral-value-store/middleware"
"github.com/dhcgn/iot-ephemeral-value-store/stats"
"github.com/dhcgn/iot-ephemeral-value-store/storage"
"github.com/gorilla/mux"
)

const (
// Server configuration
MaxRequestSize = 1024 * 10 // 10 KB for request size limit
RateLimitPerSecond = 10 // Requests per second
RateLimitBurst = 5 // Burst capability
RateLimitPerSecond = 100 // Requests per second
RateLimitBurst = 10 // Burst capability

// Database and server paths
DefaultStorePath = "./data"
Expand All @@ -44,7 +44,6 @@ var (
persistDurationString string
storePath string
port int
db *badger.DB
)

// Set in build time
Expand All @@ -70,20 +69,24 @@ func main() {
log.Fatalf("Failed to parse duration: %v", err)
}

stats := stats.NewStats()

storage := storage.NewPersistentStorage(storePath, persistDuration)
defer storage.Db.Close()

httphandlerConfig := httphandler.Config{
StorageInstance: storage,
StatsInstance: stats,
}

middlewareConfig := middleware.Config{
RateLimitPerSecond: RateLimitPerSecond,
RateLimitBurst: RateLimitBurst,
MaxRequestSize: MaxRequestSize,
StatsInstance: stats,
}

r := createRouter(httphandlerConfig, middlewareConfig)
r := createRouter(httphandlerConfig, middlewareConfig, stats)

serverAddress := fmt.Sprintf("127.0.0.1:%d", port)
srv := &http.Server{
Expand All @@ -99,7 +102,7 @@ func main() {
}
}

func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router {
func createRouter(hhc httphandler.Config, mc middleware.Config, stats *stats.Stats) *mux.Router {
// Template parsing
tmpl, err := template.ParseFS(staticFiles, "static/index.html")
if err != nil {
Expand Down Expand Up @@ -134,7 +137,22 @@ func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router {
r.HandleFunc("/delete/{uploadKey}", hhc.DeleteHandler).Methods("GET")
r.HandleFunc("/delete/{uploadKey}/", hhc.DeleteHandler).Methods("GET")

r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
r.HandleFunc("/", templateHandler(tmpl, stats))

// Not Found handler
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not Found"))
})

staticSubFS, _ := fs.Sub(staticFiles, "static")
r.PathPrefix("/").Handler(http.FileServer(http.FS(staticSubFS)))

return r
}

func templateHandler(tmpl *template.Template, stats *stats.Stats) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
key := domain.GenerateRandomKey()
key_down, err := domain.DeriveDownloadKey(key)
if err != nil {
Expand All @@ -147,20 +165,13 @@ func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router {
DataRetention: persistDurationString,
Version: Version,
BuildTime: BuildTime,
}
tmpl.Execute(w, data)
})

// Not Found handler
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not Found"))
})
Uptime: stats.GetUptime(),

staticSubFS, _ := fs.Sub(staticFiles, "static")
r.PathPrefix("/").Handler(http.FileServer(http.FS(staticSubFS)))

return r
StateData: stats.GetCurrentStats(),
}
tmpl.Execute(w, data)
}
}

type PageData struct {
Expand All @@ -169,4 +180,8 @@ type PageData struct {
DataRetention string
Version string
BuildTime string

Uptime time.Duration

StateData stats.StatsData
}
Loading

0 comments on commit 9f5dce9

Please sign in to comment.