Skip to content

Commit

Permalink
Merge pull request #39 from teknologi-umum/feat/incident-writer
Browse files Browse the repository at this point in the history
feat(backend): submit incident
  • Loading branch information
aldy505 authored May 27, 2024
2 parents d0862e9 + 5f24f28 commit 17bd6a6
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 4 deletions.
86 changes: 82 additions & 4 deletions backend/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"slices"
Expand All @@ -12,13 +11,17 @@ import (

"github.com/go-chi/chi/v5"
"github.com/rs/cors"
"github.com/rs/zerolog/log"
"github.com/unrolled/secure"
)

type Server struct {
historicalReader *MonitorHistoricalReader
centralBroker *Broker[MonitorHistorical]
incidentWriter *IncidentWriter
monitors []Monitor

apiKey string
}

type ServerConfig struct {
Expand All @@ -29,14 +32,20 @@ type ServerConfig struct {
StaticPath string
MonitorHistoricalReader *MonitorHistoricalReader
CentralBroker *Broker[MonitorHistorical]
IncidentWriter *IncidentWriter
MonitorList []Monitor

ApiKey string
}

func NewServer(config ServerConfig) *http.Server {
server := &Server{
historicalReader: config.MonitorHistoricalReader,
centralBroker: config.CentralBroker,
monitors: config.MonitorList,
incidentWriter: config.IncidentWriter,

apiKey: config.ApiKey,
}

secureMiddleware := secure.New(secure.Options{
Expand All @@ -58,10 +67,11 @@ func NewServer(config ServerConfig) *http.Server {
api.Get("/api/overview", server.snapshotOverview)
api.Get("/api/by", server.snapshotBy)
api.Get("/api/static", server.staticSnapshot)
api.Post("/api/incident", server.submitIncindent)

r := chi.NewRouter()
r.Use(secureMiddleware.Handler)
r.Handle("/api/", corsMiddleware.Handler(api))
r.Handle("/api/*", corsMiddleware.Handler(api))
r.Handle("/", http.FileServer(http.Dir(config.StaticPath)))

return &http.Server{
Expand Down Expand Up @@ -112,7 +122,6 @@ func (s *Server) snapshotOverview(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Millisecond * 10)
}
}

}

func (s *Server) snapshotBy(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -181,7 +190,6 @@ func (s *Server) snapshotBy(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Millisecond * 10)
}
}

}

func (s *Server) staticSnapshot(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -275,3 +283,73 @@ func (s *Server) staticSnapshot(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}

func (s *Server) submitIncindent(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("x-api-key")
if apiKey == "" {
w.WriteHeader(http.StatusUnauthorized)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"error": "api key is required"}`))
return
} else {
if apiKey != s.apiKey {
w.WriteHeader(http.StatusUnauthorized)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"error": "api key is invalid"}`))
return
}
}

decoder := json.NewDecoder(r.Body)
var body Incident
if err := decoder.Decode(&body); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
errBytes, marshalErr := json.Marshal(map[string]string{
"error": err.Error(),
})
if marshalErr != nil {
log.Error().Stack().Err(err).Msg("failed to marshal json")
w.Write([]byte(`{"error": "internal server error"}`))
return
}
w.Write(errBytes)
return
}
defer r.Body.Close()

if err := body.Validate(); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
errBytes, marshalErr := json.Marshal(map[string]string{
"error": err.Error(),
})
if marshalErr != nil {
log.Error().Stack().Err(err).Msg("failed to marshal json")
w.Write([]byte(`{"error": "internal server error"}`))
return
}
w.Write(errBytes)
return
}

err := s.incidentWriter.Write(r.Context(), body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
errBytes, err := json.Marshal(map[string]string{
"error": err.Error(),
})
if err != nil {
log.Error().Stack().Err(err).Msg("failed to marshal json")
w.Write([]byte(`{"error": "internal server error"}`))
return
}
w.Write(errBytes)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "success"}`))
}
72 changes: 72 additions & 0 deletions backend/incident.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"time"
)

type IncidentSeverity uint

const (
IncidentSeverityInformational IncidentSeverity = iota
IncidentSeverityWarning
IncidentSeverityError
IncidentSeverityFatal
)

func (s IncidentSeverity) IsValid() bool {
switch s {
case IncidentSeverityInformational, IncidentSeverityWarning, IncidentSeverityError, IncidentSeverityFatal:
return true
}
return false
}

type IncidentStatus uint

const (
IncidentStatusInvestigating IncidentStatus = iota
IncidentStatusIdentified
IncidentStatusMonitoring
IncidentStatusResolved
IncidentStatusScheduled
)

func (s IncidentStatus) IsValid() bool {
switch s {
case IncidentStatusInvestigating, IncidentStatusIdentified, IncidentStatusMonitoring, IncidentStatusResolved, IncidentStatusScheduled:
return true
}
return false
}

type Incident struct {
MonitorID string `json:"monitor_id"`
Title string `json:"title"`
Description string `json:"description"`
Timestamp time.Time `json:"timestamp"`
Severity IncidentSeverity `json:"severity"`
Status IncidentStatus `json:"status"`
CreatedBy string `json:"created_by"`
}

func (i Incident) Validate() error {
err := NewValidationError()

if i.Timestamp.IsZero() {
err.AddIssue("timestamp", "shouldn't be zero")
}

if !i.Severity.IsValid() {
err.AddIssue("severity", "invalid")
}

if !i.Status.IsValid() {
err.AddIssue("status", "invalid")
}

if err.HasIssues() {
return err
}

return nil
}
85 changes: 85 additions & 0 deletions backend/incident_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main_test

import (
"errors"
main "semyi"
"testing"
"time"
)

func TestIncidentValidate(t *testing.T) {
validPayload := main.Incident{
MonitorID: "a84c2c59-748c-48d0-b628-4a73b1c3a8d7",
Title: "test",
Description: "description test",
Timestamp: time.Date(2000, 7, 24, 4, 30, 15, 0, time.UTC),
Severity: main.IncidentSeverityError,
Status: main.IncidentStatusInvestigating,
}

t.Run("Should return error if payload is invalid", func(t *testing.T) {
t.Run("Timestamp", func(t *testing.T) {
validPayloadCopy := validPayload
mockTimestamps := []time.Time{
time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
}

for _, timestamp := range mockTimestamps {
validPayloadCopy.Timestamp = timestamp

err := validPayloadCopy.Validate()
if err == nil {
t.Error("expect error, got nil")
}

var expectError *main.ValidationError
if !errors.As(err, &expectError) {
t.Errorf("expect error: %T, but got : %T", expectError, err)
}
}
})
t.Run("severity", func(t *testing.T) {
validPayloadCopy := validPayload
mockSeverity := []uint{4, 5, 6}

for _, severity := range mockSeverity {
validPayloadCopy.Severity = main.IncidentSeverity(severity)

err := validPayloadCopy.Validate()
if err == nil {
t.Error("expect error, got nil")
}

var expectError *main.ValidationError
if !errors.As(err, &expectError) {
t.Errorf("expect error: %T, but got : %T", expectError, err)
}
}
})
t.Run("status", func(t *testing.T) {
validPayloadCopy := validPayload
mockStatus := []uint{5, 6, 7}

for _, status := range mockStatus {
validPayloadCopy.Status = main.IncidentStatus(status)

err := validPayloadCopy.Validate()
if err == nil {
t.Error("expect error, got nil")
}

var expectError *main.ValidationError
if !errors.As(err, &expectError) {
t.Errorf("expect error: %T, but got : %T", expectError, err)
}
}
})
})

t.Run("Shouldn't return error if payload is valid", func(t *testing.T) {
err := validPayload.Validate()
if err != nil {
t.Errorf("expect error nil, but got %v", err)
}
})
}
45 changes: 45 additions & 0 deletions backend/incident_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"context"
"database/sql"
"fmt"
"time"
)

type IncidentWriter struct {
db *sql.DB
}

func NewIncidentWriter(db *sql.DB) *IncidentWriter {
return &IncidentWriter{
db: db,
}
}

func (w *IncidentWriter) Write(ctx context.Context, incident Incident) error {
conn, err := w.db.Conn(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}

incidentStatus := incident.Status
if incident.Timestamp.After(time.Now()) {
incidentStatus = IncidentStatusScheduled
}

_, err = conn.ExecContext(ctx, "INSERT INTO incident_data (monitor_id, title, description, timestamp, severity, status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)",
incident.MonitorID,
incident.Title,
incident.Description,
incident.Timestamp,
incident.Severity,
incidentStatus,
incident.CreatedBy,
)
if err != nil {
return fmt.Errorf("failed to submit incident: %w", err)
}

return nil
}
8 changes: 8 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ func main() {
port = "5000"
}

apiKey, ok := os.LookupEnv("API_KEY")
if !ok {
log.Warn().Msg("API_KEY is not set")
}

if os.Getenv("ENV") == "" {
err := os.Setenv("ENV", "development")
if err != nil {
Expand Down Expand Up @@ -132,6 +137,9 @@ func main() {
Port: port,
StaticPath: staticPath,
MonitorHistoricalReader: NewMonitorHistoricalReader(db),
IncidentWriter: NewIncidentWriter(db),

ApiKey: apiKey,
})
go func() {
// Listen for SIGKILL and SIGTERM
Expand Down

0 comments on commit 17bd6a6

Please sign in to comment.