Working dev and health endpoint

main
Drew Bednar 4 weeks ago
parent f5e555b7ff
commit 1d3c1de858

@ -3,9 +3,9 @@ testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = ["-logging=DEBUG"]
args_bin = ["-log-level=DEBUG"]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main cmd/api/main.go"
cmd = "go build -o ./tmp/main ./cmd/api/..."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
@ -14,7 +14,7 @@ tmp_dir = "tmp"
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "go.tmpl"]
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "1s"
log = "build-errors.log"
@ -22,7 +22,7 @@ tmp_dir = "tmp"
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun = false
rerun_delay = 500
send_interrupt = true
stop_on_error = false

1
.gitignore vendored

@ -21,3 +21,4 @@
# Go workspace file
go.work
tmp/

@ -23,7 +23,6 @@ test-int:
go test $(FLAGS) ./cmd/...
.PHONY: test-int
## Coverage See also -covermode=count and -covermode=atomic
cover-html: test-cover
go tool cover -html=$(COVER_PROFILE)
@ -38,7 +37,7 @@ test-cover:
.PHONY: test-cover
serve:
go run ./cmd/api/main.go
go run ./cmd/api/...
.PHONY: serve
# SQLite Commands

@ -0,0 +1,6 @@
```
[build]
kill_delay = "1s"
send_interrupt = true
```

1
bin/.gitignore vendored

@ -0,0 +1 @@
main

@ -0,0 +1,12 @@
package main
import (
"fmt"
"net/http"
)
func (app *application) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "status: available!")
fmt.Fprintf(w, "environment: %s\n", app.config.env)
fmt.Fprintf(w, "version: %s\n", Version)
}

@ -1,15 +1,100 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.runcible.io/learning/pulley/internal/logging"
)
func Run(w io.Writer) {
fmt.Fprint(w, "Hello API\n")
const Version = "1.0.0"
type config struct {
port int
env string
logLevel string
}
type application struct {
config config
logger *slog.Logger
}
func run(ctx context.Context, w io.Writer, args []string) error {
var cfg config
flagSet := flag.NewFlagSet(args[0], flag.ExitOnError)
flagSet.IntVar(&cfg.port, "port", 5002, "API server port")
flagSet.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
flagSet.StringVar(&cfg.logLevel, "log-level", "INFO", "Logging Level (INFO|DEBUG|WARN|ERROR)")
if err := flagSet.Parse(args[1:]); err != nil {
return err
}
logger := logging.InitLogging(cfg.logLevel, w, true)
app := application{config: cfg, logger: logger}
mux := http.NewServeMux()
mux.HandleFunc("/v1/healthcheck", app.HealthCheckHandler)
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", "0.0.0.0", app.config.port),
ErrorLog: slog.NewLogLogger(app.logger.Handler(), slog.LevelError),
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 524288, // 0.5 mb
Handler: mux,
}
slog.Info(fmt.Sprintf("Listening on http://%s", srv.Addr))
defer func() {
slog.Debug("Must have hit a graceful shutdown")
}()
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
slog.Error(err.Error())
os.Exit(1)
slog.Info("Stopped serving connections")
}
}()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Block until signal is received
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("Failed to close within context timeout. Forcing server close.")
srv.Close()
return err
}
return nil
}
func main() {
Run(os.Stdout)
ctx := context.Background()
if err := run(ctx, os.Stdout, os.Args); err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}

@ -1,16 +1,15 @@
package main
import (
"bytes"
"testing"
)
func TestRun(t *testing.T) {
buffer := bytes.Buffer{}
Run(&buffer)
got := buffer.String()
want := "Hello API\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
// buffer := bytes.Buffer{}
// run(&buffer)
// got := buffer.String()
// want := "Hello API\n"
// if got != want {
// t.Errorf("got %q, want %q", got, want)
// }
}

@ -1,3 +1,4 @@
module git.runcible.io/learning/pulley
go 1.24.1
//go 1.24.1
go 1.23.3

@ -0,0 +1,37 @@
package logging
import (
"io"
"log/slog"
"os"
"strings"
)
func parseLogLevel(levelStr string) slog.Level {
switch strings.ToUpper(levelStr) {
case "DEBUG":
return slog.LevelDebug
case "INFO":
return slog.LevelInfo
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
default:
return slog.LevelInfo // Default level
}
}
// InitLogggin initializes global structured logging for the entire application
func InitLogging(level string, w io.Writer, addSource bool) *slog.Logger {
// Use os.Stderr
//
// Stderr is used for diagnostics and logging. Stdout is used for program
// output. Stderr also have greater likely hood of being seen if a programs
// output is being redirected.
parsedLogLevel := parseLogLevel(level)
loggerHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: addSource})
logger := slog.New(loggerHandler)
slog.SetDefault(logger)
return logger
}
Loading…
Cancel
Save