Skip to content

Instantly share code, notes, and snippets.

@aphexddb
Last active May 30, 2018 18:36
Show Gist options
  • Save aphexddb/378d0f81056716150ad4cede2256f7e4 to your computer and use it in GitHub Desktop.
Save aphexddb/378d0f81056716150ad4cede2256f7e4 to your computer and use it in GitHub Desktop.
Golang handler patterns

Example of using handlers in Golang to support a shared environment such as a logger, tracing and seperation of business logic from handlers.

To run a jaeger collector and UI run the following:

docker run -d -e \
  COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
opentracing "github.com/opentracing/opentracing-go"
)
// errors
var (
ErrAgentNotFound = errors.New("Agent not found")
)
// Env represents a service environment
type Env struct {
logger *logrus.Entry
hostPort string
tracer opentracing.Tracer
}
// Middleware defines HTTP middleware
type Middleware func(next http.HandlerFunc) http.HandlerFunc
// HealthCheckResponse contains a health check response
type HealthCheckResponse struct {
Status string `json:"status"`
}
// Agent represents a secret aget
type Agent struct {
ID string `json:"id"`
Name string `json:"name"`
}
// AgentResponse contains an agent request response
type AgentResponse struct {
Agent Agent `json:"agent"`
Status string `json:"status"`
}
// AgentAction perform an agent action
func AgentAction() (*AgentResponse, error) {
agent := &AgentResponse{
Agent{
ID: "007",
Name: "James Bond",
},
"License to Kill",
}
return agent, nil
}
// IndexHandler serves up an index file for every request
func IndexHandler(e *Env) func(w http.ResponseWriter, r *http.Request) {
logger := e.logger.WithField("handler", "IndexHandler")
return func(w http.ResponseWriter, r *http.Request) {
logger.Debug("index called")
w.Write([]byte("Hello World."))
}
}
// HealthCheckHandler returns health check status
func HealthCheckHandler(e *Env) func(w http.ResponseWriter, r *http.Request) {
logger := e.logger.WithField("handler", "IndexHandler")
return func(w http.ResponseWriter, r *http.Request) {
logger.Debug("health check ok")
j, _ := json.Marshal(HealthCheckResponse{
Status: "ok",
})
w.Write(j)
}
}
// AgentStatusHandler returns information on an agent
func AgentStatusHandler(e *Env) func(w http.ResponseWriter, r *http.Request) {
logger := e.logger.WithField("handler", "AgentStatusHandler")
return func(w http.ResponseWriter, r *http.Request) {
if span := opentracing.SpanFromContext(r.Context()); span != nil {
subSpan := opentracing.StartSpan(
"AgentStatusHandler",
opentracing.ChildOf(span.Context()))
defer subSpan.Finish()
}
logger.Debug("Agent status handler called")
NoopOperation(r.Context())
agent, err := AgentAction()
if err == ErrAgentNotFound {
logger.Debug(ErrAgentNotFound.Error)
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusNotFound)
return
}
if err != nil {
logger.Error(ErrAgentNotFound.Error())
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
j, _ := json.Marshal(agent)
w.Write(j)
}
}
// NoopOperation performs a noop
func NoopOperation(ctx context.Context) {
if span := opentracing.SpanFromContext(ctx); span != nil {
subSpan := opentracing.StartSpan(
"NoopOperation",
opentracing.ChildOf(span.Context()))
defer subSpan.Finish()
subSpan.SetTag("foo", "bar")
}
}
const jaegerServiceName = "agent_service"
const jaegerHostPort = "192.168.99.100:5775"
func main() {
// logger
log := logrus.New()
log.SetLevel(logrus.DebugLevel)
log.Formatter = &logrus.JSONFormatter{}
logger := log.WithField("type", "handler")
tracer, closer, tracerErr := NewJaegerTracer(jaegerServiceName, jaegerHostPort, false)
if tracerErr != nil {
log.Panic(tracerErr)
}
defer closer.Close()
// setup environment
env := &Env{
logger: logger,
hostPort: "8000",
tracer: tracer,
}
// routes
r := mux.NewRouter()
// API
api := r.PathPrefix("/v1").Subrouter()
api.HandleFunc("/health", HealthCheckHandler(env)).Methods("GET")
api.HandleFunc("/agent", env.TracingMiddleware(AgentStatusHandler(env))).Methods("GET")
r.PathPrefix("/").HandlerFunc(IndexHandler(env)).Methods("GET")
// print out routes before starting
fmt.Println("Routes:")
r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
t, err := route.GetPathTemplate()
if err != nil {
return err
}
fmt.Printf(" %s\n", t)
return nil
})
fmt.Println("Listening on :8000")
log.Fatal(http.ListenAndServe(":8000", r))
}
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
opentracing "github.com/opentracing/opentracing-go"
jaeger "github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-lib/metrics/prometheus"
)
// TracingResponseWriter wraps http.ResponseWriter
type TracingResponseWriter struct {
w http.ResponseWriter
status int
}
// NewTracingResponseWriter returns a new response writer that tracks the status code
func NewTracingResponseWriter(w http.ResponseWriter) *TracingResponseWriter {
return &TracingResponseWriter{w, http.StatusOK}
}
// WriteHeader wraps the underlying WriteHeader method
func (trw *TracingResponseWriter) WriteHeader(status int) {
trw.status = status
trw.w.WriteHeader(status)
}
// Write wraps the underlying Write method
func (trw *TracingResponseWriter) Write(b []byte) (int, error) {
return trw.w.Write(b)
}
// Header wraps the underlying Header method
func (trw *TracingResponseWriter) Header() http.Header {
return trw.w.Header()
}
// Status returns the status code
func (trw *TracingResponseWriter) Status() int {
return trw.status
}
// TracingMiddleware implements tracing for HTTP requests
func (e *Env) TracingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
operationName := fmt.Sprintf("http.%s", strings.ToLower(r.Method))
// retrieve current Span from Context if it exists
var parentCtx opentracing.SpanContext
parentSpan := opentracing.SpanFromContext(r.Context())
if parentSpan != nil {
parentCtx = parentSpan.Context()
}
// start a new Span to wrap HTTP request
span := e.tracer.StartSpan(
operationName,
opentracing.ChildOf(parentCtx),
)
span.SetTag("http.url", r.URL.String())
// make sure the Span is finished once we're done
defer span.Finish()
// make the Span current in the context
ctx := opentracing.ContextWithSpan(r.Context(), span)
// wrap the response writer
trw := NewTracingResponseWriter(w)
// pass the span context to the next handler
next.ServeHTTP(trw, r.WithContext(ctx))
// set the status code that was written
span.SetTag("http.status", trw.Status())
}
}
// NewJaegerTracer returns a new Jaeger tracer
func NewJaegerTracer(serviceName, hostPort string, production bool) (opentracing.Tracer, io.Closer, error) {
cfg := config.Configuration{}
// Configuration for testing. Uses constant sampling to sample every trace
// and enable LogSpan to log every span via configured Logger.
if !production {
cfg = config.Configuration{
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &config.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: hostPort,
},
}
}
// By default, a no-op metrics.NullFactory is used
metricsFactory := prometheus.New()
tracer, closer, err := cfg.New(
serviceName,
config.Logger(jaeger.StdLogger),
config.Metrics(metricsFactory),
)
if err != nil {
return nil, nil, err
}
// set the global tracer
opentracing.SetGlobalTracer(tracer)
return tracer, closer, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment