Skip to content

Commit cbc5063

Browse files
committed
Clean up error handling pattern
1 parent 03d2b46 commit cbc5063

File tree

16 files changed

+1087
-131
lines changed

16 files changed

+1087
-131
lines changed

PATTERNS.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# Code Patterns & Best Practices
2+
3+
This document describes the coding patterns used in this project, matching the patterns from kernel/packages/api.
4+
5+
## Project Structure
6+
7+
```
8+
cmd/api/ # Main application
9+
├── api/ # API handlers (one file per resource)
10+
│ ├── api.go # ApiService struct + New() constructor
11+
│ ├── health.go # Health check
12+
│ ├── images.go # Image resource handlers
13+
│ ├── instances.go # Instance resource handlers
14+
│ └── volumes.go # Volume resource handlers
15+
├── config/ # Configuration
16+
│ └── config.go
17+
├── main.go # Entry point, server setup
18+
├── wire.go # Dependency injection definition
19+
└── wire_gen.go # Generated by wire
20+
21+
lib/ # Service layer
22+
├── images/
23+
│ ├── errors.go # Sentinel errors
24+
│ └── manager.go # Business logic
25+
├── instances/
26+
│ ├── errors.go
27+
│ └── manager.go
28+
├── volumes/
29+
│ ├── errors.go
30+
│ └── manager.go
31+
├── logger/ # Context-based logging
32+
│ └── logger.go
33+
├── providers/ # Wire providers
34+
│ └── providers.go
35+
└── oapi/ # Generated from OpenAPI spec
36+
└── oapi.go
37+
```
38+
39+
## Error Handling Pattern
40+
41+
### Service Layer (lib/)
42+
43+
**Rules:**
44+
- Accept `context.Context`, never `*http.Request` or HTTP types
45+
- Return sentinel errors (e.g., `ErrNotFound`, `ErrAlreadyExists`)
46+
- Use `fmt.Errorf("context: %w", err)` to wrap errors
47+
48+
**Example (lib/images/errors.go):**
49+
```go
50+
package images
51+
52+
import "errors"
53+
54+
var (
55+
ErrNotFound = errors.New("image not found")
56+
ErrAlreadyExists = errors.New("image already exists")
57+
)
58+
```
59+
60+
**Example (lib/images/manager.go):**
61+
```go
62+
func (m *manager) GetImage(ctx context.Context, id string) (*oapi.Image, error) {
63+
exists := false // TODO: actual check
64+
if !exists {
65+
return nil, ErrNotFound // Return sentinel error
66+
}
67+
68+
// If something else fails:
69+
err := doSomething()
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to load image metadata: %w", err)
72+
}
73+
74+
return image, nil
75+
}
76+
```
77+
78+
### Handler Layer (cmd/api/api/)
79+
80+
**Rules:**
81+
- Extract logger from context: `log := logger.FromContext(ctx)`
82+
- Use inline `switch` with `errors.Is()` to map errors → HTTP responses
83+
- Default case → 500 + log the error
84+
- Never expose internal error messages to clients
85+
86+
**Example (cmd/api/api/images.go):**
87+
```go
88+
func (s *ApiService) GetImage(ctx context.Context, request oapi.GetImageRequestObject) (oapi.GetImageResponseObject, error) {
89+
log := logger.FromContext(ctx)
90+
91+
img, err := s.ImageManager.GetImage(ctx, request.Id)
92+
if err != nil {
93+
switch {
94+
case errors.Is(err, images.ErrNotFound):
95+
return oapi.GetImage404JSONResponse{
96+
Code: "not_found",
97+
Message: "image not found",
98+
}, nil
99+
default:
100+
log.Error("failed to get image", "error", err, "id", request.Id)
101+
return oapi.GetImage500JSONResponse{
102+
Code: "internal_error",
103+
Message: "failed to get image",
104+
}, nil
105+
}
106+
}
107+
return oapi.GetImage200JSONResponse(*img), nil
108+
}
109+
```
110+
111+
## Logging Pattern
112+
113+
### Setup (lib/logger/logger.go)
114+
115+
```go
116+
package logger
117+
118+
import (
119+
"context"
120+
"log/slog"
121+
)
122+
123+
type contextKey string
124+
125+
const loggerKey contextKey = "logger"
126+
127+
func AddToContext(ctx context.Context, logger *slog.Logger) context.Context {
128+
return context.WithValue(ctx, loggerKey, logger)
129+
}
130+
131+
func FromContext(ctx context.Context) *slog.Logger {
132+
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
133+
return logger
134+
}
135+
return slog.Default()
136+
}
137+
```
138+
139+
### Usage in Providers
140+
141+
```go
142+
func ProvideLogger() *slog.Logger {
143+
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
144+
Level: slog.LevelInfo,
145+
}))
146+
}
147+
148+
func ProvideContext(log *slog.Logger) context.Context {
149+
return logger.AddToContext(context.Background(), log)
150+
}
151+
```
152+
153+
### Usage in Handlers
154+
155+
```go
156+
func (s *ApiService) SomeHandler(ctx context.Context, ...) (..., error) {
157+
log := logger.FromContext(ctx)
158+
159+
log.Info("doing something", "key", "value")
160+
log.Error("something failed", "error", err, "id", someId)
161+
}
162+
```
163+
164+
## Dependency Injection (Wire)
165+
166+
### Define Providers (lib/providers/providers.go)
167+
168+
```go
169+
func ProvideLogger() *slog.Logger { ... }
170+
func ProvideContext(log *slog.Logger) context.Context { ... }
171+
func ProvideConfig() *config.Config { ... }
172+
func ProvideImageManager(cfg *config.Config) images.Manager { ... }
173+
```
174+
175+
### Wire Definition (cmd/api/wire.go)
176+
177+
```go
178+
//go:build wireinject
179+
180+
package main
181+
182+
import (
183+
"github.com/google/wire"
184+
...
185+
)
186+
187+
type application struct {
188+
Ctx context.Context
189+
Logger *slog.Logger
190+
Config *config.Config
191+
ImageManager images.Manager
192+
...
193+
ApiService *api.ApiService
194+
}
195+
196+
func initializeApp() (*application, func(), error) {
197+
panic(wire.Build(
198+
providers.ProvideLogger, // Order matters! Logger first
199+
providers.ProvideContext, // Then context (depends on logger)
200+
providers.ProvideConfig,
201+
providers.ProvideImageManager,
202+
...
203+
api.New,
204+
wire.Struct(new(application), "*"),
205+
))
206+
}
207+
```
208+
209+
### Generate Wire Code
210+
211+
```bash
212+
make generate-wire
213+
```
214+
215+
## Code Generation
216+
217+
### OpenAPI
218+
219+
After modifying `openapi.yaml`:
220+
```bash
221+
make oapi-generate
222+
```
223+
224+
### Wire
225+
226+
After modifying `wire.go` or providers:
227+
```bash
228+
make generate-wire
229+
```
230+
231+
### Both
232+
233+
```bash
234+
make generate-all
235+
```
236+
237+
## Testing Pattern
238+
239+
(TODO: To be established when implementing actual functionality)
240+
241+
```go
242+
package images_test
243+
244+
import (
245+
"context"
246+
"testing"
247+
248+
"github.com/onkernel/cloud-hypervisor-dataplane/lib/images"
249+
"github.com/stretchr/testify/require"
250+
)
251+
252+
func TestGetImage_NotFound(t *testing.T) {
253+
mgr := images.NewManager("/tmp/test")
254+
_, err := mgr.GetImage(context.Background(), "fake-id")
255+
require.ErrorIs(t, err, images.ErrNotFound)
256+
}
257+
```
258+
259+
## Comments
260+
261+
From workspace rules:
262+
263+
- Keep comments simple and free of extra formatting
264+
- Avoid obvious comments
265+
- Explain the next big chunk of code at a high level
266+
- Always add comments for exports in Go
267+
- Never make comments that refer to facts only present in the prompt
268+
269+
**Good:**
270+
```go
271+
// Manager handles image lifecycle operations
272+
type Manager interface { ... }
273+
```
274+
275+
**Bad:**
276+
```go
277+
// ---
278+
// 1. Get image from disk
279+
// 2. Parse metadata
280+
// ---
281+
```
282+
283+
## Summary
284+
285+
Key principles:
286+
1. **Separation of concerns**: Handlers in `cmd/api/api/`, business logic in `lib/`
287+
2. **Clean error handling**: Sentinel errors + switch statements + default to 500
288+
3. **Context-based logging**: Extract logger from context in handlers
289+
4. **Dependency injection**: Wire for clean initialization
290+
5. **Code generation**: OpenAPI spec → Go code, Wire → dependency injection
291+
6. **No HTTP in services**: Service layer is HTTP-agnostic
292+
293+
These patterns match kernel/packages/api and provide a clean, maintainable codebase.

0 commit comments

Comments
 (0)