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

1
.gitignore vendored

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

@ -23,7 +23,6 @@ test-int:
go test $(FLAGS) ./cmd/... go test $(FLAGS) ./cmd/...
.PHONY: test-int .PHONY: test-int
## Coverage See also -covermode=count and -covermode=atomic ## Coverage See also -covermode=count and -covermode=atomic
cover-html: test-cover cover-html: test-cover
go tool cover -html=$(COVER_PROFILE) go tool cover -html=$(COVER_PROFILE)
@ -38,7 +37,7 @@ test-cover:
.PHONY: test-cover .PHONY: test-cover
serve: serve:
go run ./cmd/api/main.go go run ./cmd/api/...
.PHONY: serve .PHONY: serve
# SQLite Commands # 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 package main
import ( import (
"context"
"errors"
"flag"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http"
"os" "os"
"os/signal"
"syscall"
"time"
"git.runcible.io/learning/pulley/internal/logging"
) )
func Run(w io.Writer) { const Version = "1.0.0"
fmt.Fprint(w, "Hello API\n")
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() { 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 package main
import ( import (
"bytes"
"testing" "testing"
) )
func TestRun(t *testing.T) { func TestRun(t *testing.T) {
buffer := bytes.Buffer{} // buffer := bytes.Buffer{}
Run(&buffer) // run(&buffer)
got := buffer.String() // got := buffer.String()
want := "Hello API\n" // want := "Hello API\n"
if got != want { // if got != want {
t.Errorf("got %q, want %q", got, want) // t.Errorf("got %q, want %q", got, want)
} // }
} }

@ -1,3 +1,4 @@
module git.runcible.io/learning/pulley 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