|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bufio" |
| 5 | + "context" |
| 6 | + "encoding/json" |
| 7 | + "errors" |
| 8 | + "fmt" |
| 9 | + "log" |
| 10 | + "net" |
| 11 | + "net/http" |
| 12 | + "net/url" |
| 13 | + "os" |
| 14 | + "strings" |
| 15 | + "time" |
| 16 | +) |
| 17 | + |
| 18 | +type Todo struct { |
| 19 | + ID int `json:"id"` |
| 20 | + Title string `json:"title"` |
| 21 | + Completed bool `json:"completed"` |
| 22 | +} |
| 23 | + |
| 24 | +func init() { |
| 25 | + declaration() |
| 26 | +} |
| 27 | + |
| 28 | +func main() { |
| 29 | + mux := http.NewServeMux() |
| 30 | + mux.HandleFunc("/ready", readinessHandler) |
| 31 | + mux.HandleFunc("/demo", demoHandler) |
| 32 | + |
| 33 | + addr := ":" + envOrDefault("PORT", "8080") |
| 34 | + log.Printf("request-logger demo listening on %s", addr) |
| 35 | + if err := http.ListenAndServe(addr, mux); err != nil { |
| 36 | + log.Fatal(err) |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +func readinessHandler(w http.ResponseWriter, r *http.Request) { |
| 41 | + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) |
| 42 | + defer cancel() |
| 43 | + |
| 44 | + if err := checkTodosAPI(ctx); err != nil { |
| 45 | + http.Error(w, err.Error(), http.StatusServiceUnavailable) |
| 46 | + return |
| 47 | + } |
| 48 | + if err := checkRedis(ctx); err != nil { |
| 49 | + http.Error(w, err.Error(), http.StatusServiceUnavailable) |
| 50 | + return |
| 51 | + } |
| 52 | + w.WriteHeader(http.StatusNoContent) |
| 53 | +} |
| 54 | + |
| 55 | +func demoHandler(w http.ResponseWriter, r *http.Request) { |
| 56 | + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) |
| 57 | + defer cancel() |
| 58 | + |
| 59 | + result := map[string]string{ |
| 60 | + "todosApi": statusString(checkTodosAPI(ctx)), |
| 61 | + "cache": statusString(checkRedis(ctx)), |
| 62 | + } |
| 63 | + |
| 64 | + status := http.StatusOK |
| 65 | + if result["todosApi"] != "ok" || result["cache"] != "ok" { |
| 66 | + status = http.StatusInternalServerError |
| 67 | + } |
| 68 | + |
| 69 | + w.Header().Set("Content-Type", "application/json") |
| 70 | + w.WriteHeader(status) |
| 71 | + if status == http.StatusInternalServerError { |
| 72 | + w.Write([]byte(fmt.Sprintf("One or more dependencies are not healthy: todosApi=%s, cache=%s", result["todosApi"], result["cache"]))) |
| 73 | + } |
| 74 | + if err := json.NewEncoder(w).Encode(result); err != nil { |
| 75 | + log.Printf("encode response: %v", err) |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +func checkTodosAPI(ctx context.Context) error { |
| 80 | + baseURL := strings.TrimRight(os.Getenv("TODOS_API_URL"), "/") |
| 81 | + if baseURL == "" { |
| 82 | + return errors.New("TODOS_API_URL is not set") |
| 83 | + } |
| 84 | + |
| 85 | + request, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/todos/1", nil) |
| 86 | + if err != nil { |
| 87 | + return err |
| 88 | + } |
| 89 | + response, err := http.DefaultClient.Do(request) |
| 90 | + if err != nil { |
| 91 | + return fmt.Errorf("todos-api request failed: %w", err) |
| 92 | + } |
| 93 | + defer response.Body.Close() |
| 94 | + if response.StatusCode != http.StatusOK { |
| 95 | + return fmt.Errorf("todos-api returned %s", response.Status) |
| 96 | + } |
| 97 | + |
| 98 | + var todo Todo |
| 99 | + if err := json.NewDecoder(response.Body).Decode(&todo); err != nil { |
| 100 | + return fmt.Errorf("todos-api response decode failed: %w", err) |
| 101 | + } |
| 102 | + if todo.ID == 0 || todo.Title == "" { |
| 103 | + return errors.New("todos-api response was incomplete") |
| 104 | + } |
| 105 | + return nil |
| 106 | +} |
| 107 | + |
| 108 | +func checkRedis(ctx context.Context) error { |
| 109 | + addr, err := redisAddress() |
| 110 | + if err != nil { |
| 111 | + return err |
| 112 | + } |
| 113 | + |
| 114 | + var dialer net.Dialer |
| 115 | + conn, err := dialer.DialContext(ctx, "tcp", addr) |
| 116 | + if err != nil { |
| 117 | + return fmt.Errorf("redis dial failed: %w", err) |
| 118 | + } |
| 119 | + defer conn.Close() |
| 120 | + |
| 121 | + deadline, ok := ctx.Deadline() |
| 122 | + if ok { |
| 123 | + _ = conn.SetDeadline(deadline) |
| 124 | + } |
| 125 | + if _, err := conn.Write([]byte("*1\r\n$4\r\nPING\r\n")); err != nil { |
| 126 | + return fmt.Errorf("redis ping write failed: %w", err) |
| 127 | + } |
| 128 | + line, err := bufio.NewReader(conn).ReadString('\n') |
| 129 | + if err != nil { |
| 130 | + return fmt.Errorf("redis ping read failed: %w", err) |
| 131 | + } |
| 132 | + if !strings.HasPrefix(line, "+PONG") { |
| 133 | + return fmt.Errorf("redis ping returned %q", strings.TrimSpace(line)) |
| 134 | + } |
| 135 | + return nil |
| 136 | +} |
| 137 | + |
| 138 | +func redisAddress() (string, error) { |
| 139 | + if rawURL := os.Getenv("REDIS_URL"); rawURL != "" { |
| 140 | + parsed, err := url.Parse(rawURL) |
| 141 | + if err != nil { |
| 142 | + return "", fmt.Errorf("invalid REDIS_URL: %w", err) |
| 143 | + } |
| 144 | + if parsed.Host != "" { |
| 145 | + return parsed.Host, nil |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + host := os.Getenv("REDIS_HOST") |
| 150 | + port := envOrDefault("REDIS_PORT", "6379") |
| 151 | + if host == "" { |
| 152 | + return "", errors.New("REDIS_URL or REDIS_HOST must be set") |
| 153 | + } |
| 154 | + return net.JoinHostPort(host, port), nil |
| 155 | +} |
| 156 | + |
| 157 | +func statusString(err error) string { |
| 158 | + if err != nil { |
| 159 | + log.Print(err) |
| 160 | + return "error" |
| 161 | + } |
| 162 | + return "ok" |
| 163 | +} |
| 164 | + |
| 165 | +func envOrDefault(key, fallback string) string { |
| 166 | + value := os.Getenv(key) |
| 167 | + if value == "" { |
| 168 | + return fallback |
| 169 | + } |
| 170 | + return value |
| 171 | +} |
0 commit comments