Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): submit incident #39

Merged
merged 5 commits into from
May 27, 2024
Merged
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
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
}
YogiPristiawan marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


_, 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
Loading