Skip to content

Commit

Permalink
Merge 80c2b14 into 2868a66
Browse files Browse the repository at this point in the history
  • Loading branch information
jlorgal committed Oct 3, 2018
2 parents 2868a66 + 80c2b14 commit ec242c1
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 31 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,38 @@ This example creates the following log records:

Note that the log context passed to **WithLogContext** middleware must follow the type **govice.LogContext**. This is required because the middleware sets the transactionID and correlator in this context.

The `Pipeline` simplifies the creation of a list of middlewares. The previous example would be:

```go
package main

import (
"fmt"
"net/http"
"time"

"github.com/Telefonica/govice"
)

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello world")
}

func main() {
// Force UTC time zone (used in time field of the log records)
time.Local = time.UTC
// Create the context for the logger instance
ctxt := govice.LogContext{Service: "logger", Component: "demo"}
// Create the list of middlewares for the pipeline (excluding the last handler)
mws := []func(http.HandlerFunc) http.HandlerFunc{
govice.WithLogContext(&ctxt),
govice.WithLog,
}
http.HandleFunc("/", govice.Pipeline(mws, handler))
http.ListenAndServe(":8080", nil)
}
```

## Errors and alarms

This library defines some custom errors. Errors store information for logging, and to generate the HTTP response.
Expand Down Expand Up @@ -271,6 +303,20 @@ The function `func ReplyWithError(w http.ResponseWriter, r *http.Request, err er
- It generates a HTTP response using a standard error. If the error is of type **govice.Error**, then it is casted to retrieve all the information; otherwise, it replies with a server error.
- It also logs the error using the logger in the request context. Note that it depends on the **WithLogContext** middleware. If the status code associated to the error is 4xx, then it is logged with **INFO** level; otherwise, with **ERROR** level. If the error contains an alarm identifier, it is also logged.

## Additional utilities

It provides a simple utility to create a HTTP JSON response by following 2 steps:
- Add the `Content-Type` header to `application/json`
- Marshal a golang type to JSON. If the marshalling fails, it replies with a govice error.

```go
func handler(w http.ResponseWriter, r *http.Request) {
// Object to be serialized to JSON in the HTTP response
resp := govice.LogContext{Service: "logger", Component: "demo"}
govice.WriteJSON(w, r, &resp)
}
```

## License

Copyright 2017 [Telefónica Investigación y Desarrollo, S.A.U](http://www.tid.es)
Expand Down
36 changes: 36 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@

package govice

// Context to support extending the log context with other parameters.
type Context interface {
Clone() Context
GetCorrelator() string
SetCorrelator(corr string)
GetTransactionID() string
SetTransactionID(trans string)
}

// LogContext represents the log context for a base service.
// Note that LogContext implements the Context interface.
type LogContext struct {
TransactionID string `json:"trans,omitempty"`
Correlator string `json:"corr,omitempty"`
Expand All @@ -29,6 +39,32 @@ type LogContext struct {
Alarm string `json:"alarm,omitempty"`
}

// Clone the log context.
func (c *LogContext) Clone() Context {
a := *c
return &a
}

// GetCorrelator returns the log context correlator.
func (c *LogContext) GetCorrelator() string {
return c.Correlator
}

// SetCorrelator to set a correlator in the log context.
func (c *LogContext) SetCorrelator(corr string) {
c.Correlator = corr
}

// GetTransactionID returns the log context transactionID (trans).
func (c *LogContext) GetTransactionID() string {
return c.TransactionID
}

// SetTransactionID to set a transactionID in the log context.
func (c *LogContext) SetTransactionID(trans string) {
c.TransactionID = trans
}

// ReqLogContext is a complementary LogContext to log information about the request (e.g. path).
type ReqLogContext struct {
Method string `json:"method,omitempty"`
Expand Down
21 changes: 13 additions & 8 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,24 @@ var NotFoundError = &Error{

// ReplyWithError to send a HTTP response with the error document.
func ReplyWithError(w http.ResponseWriter, r *http.Request, err error) {
logger := GetLogger(r)
switch e := err.(type) {
case *Error:
if e.Status >= http.StatusBadRequest && e.Status < http.StatusInternalServerError {
GetLogger(r).Info(err.Error())
} else if e.Alarm != "" {
logContext := LogContext{Alarm: e.Alarm}
GetLogger(r).ErrorC(logContext, err.Error())
} else {
GetLogger(r).Error(err.Error())
if logger != nil {
if e.Status >= http.StatusBadRequest && e.Status < http.StatusInternalServerError {
logger.Info(err.Error())
} else if e.Alarm != "" {
logContext := LogContext{Alarm: e.Alarm}
logger.ErrorC(logContext, err.Error())
} else {
logger.Error(err.Error())
}
}
e.Response(w)
default:
GetLogger(r).Error(err.Error())
if logger != nil {
logger.Error(err.Error())
}
NewServerError("").Response(w)
}
}
33 changes: 33 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2017 Telefónica Investigación y Desarrollo, S.A.U
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package govice

import (
"encoding/json"
"net/http"
)

// WriteJSON generates a HTTP response by marshalling an object to JSON.
// If the marshalling fails, it generates a govice error.
// It also sets the HTTP header "Content-Type" to "application/json".
func WriteJSON(w http.ResponseWriter, r *http.Request, v interface{}) {
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
ReplyWithError(w, r, err)
}
}
48 changes: 48 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2017 Telefónica Investigación y Desarrollo, S.A.U
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package govice

import (
"net/http/httptest"
"testing"
)

func TestWriteJSON(t *testing.T) {
tcs := []struct {
v interface{}
expectedBody string
expectedCode int
}{
{&LogContext{TransactionID: "txid-01", Correlator: "corr-02"}, `{"trans":"txid-01","corr":"corr-02"}` + "\n", 200},
{make(chan int), `{"error":"server_error"}`, 500},
}
for _, tc := range tcs {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/users", nil)
WriteJSON(w, r, tc.v)
if w.Body.String() != tc.expectedBody {
t.Errorf("Invalid JSON body. Expected: %s. Got: %s.", tc.expectedBody, w.Body)
}
if w.Code != tc.expectedCode {
t.Errorf("Invalid status code. Expected: %d. Got: %d.", tc.expectedCode, w.Code)
}
if w.Header().Get("Content-Type") != "application/json" {
t.Errorf("Invalid Content-Type HTTP header. Got: %s.", w.Header().Get("Content-Type"))
}
}
}
45 changes: 24 additions & 21 deletions mw.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"time"

"github.com/google/uuid"
"github.com/imdario/mergo"
)

// CorrelatorHTTPHeader contains the name of the HTTP header that transports the correlator.
Expand Down Expand Up @@ -60,28 +59,29 @@ func newTransactionID() string {
return UUID.String()
}

func newContextLogger(r *http.Request, ctx *LogContext) *Logger {
logger := NewLogger()
newCtx := NewType(ctx).(*LogContext)
logger.SetLogContext(newCtx)
if err := mergo.Merge(newCtx, ctx); err != nil {
return logger
// InitContext clones the context (to avoid reusing the same context attributes from previous requests)
// and initializes the transactionId and correlator.
func InitContext(r *http.Request, ctxt Context) Context {
newCtxt := ctxt.Clone()
trans := newTransactionID()
corr := r.Header.Get(CorrelatorHTTPHeader)
if corr == "" {
corr = trans
r.Header.Add(CorrelatorHTTPHeader, corr)
}
newCtx.TransactionID = newTransactionID()
if newCtx.Correlator = r.Header.Get(CorrelatorHTTPHeader); newCtx.Correlator == "" {
newCtx.Correlator = newCtx.TransactionID
r.Header.Add(CorrelatorHTTPHeader, newCtx.Correlator)
}
return logger
newCtxt.SetTransactionID(trans)
newCtxt.SetCorrelator(corr)
return newCtxt
}

// WithLogContext is a middleware constructor to initialize the log context with the
// transactionID and correlator. It also stores the logger in the golang context.
// Note that the context is initialized with an initial context (see ctx).
func WithLogContext(ctx *LogContext) func(http.HandlerFunc) http.HandlerFunc {
// Note that the context is initialized with an initial context (see ctxt).
func WithLogContext(ctxt Context) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := newContextLogger(r, ctx)
logger := NewLogger()
logger.SetLogContext(InitContext(r, ctxt))
next(w, r.WithContext(context.WithValue(r.Context(), LoggerContextKey, logger)))
}
}
Expand All @@ -95,10 +95,11 @@ func WithLog(next http.HandlerFunc) http.HandlerFunc {
logger := GetLogger(r)
isNewLogger := false
if logger == nil {
logger = newContextLogger(r, &LogContext{})
logger := NewLogger()
logger.SetLogContext(InitContext(r, &LogContext{}))
isNewLogger = true
}
logContext := logger.GetLogContext().(*LogContext)
logContext := logger.GetLogContext().(Context)
reqContext := ReqLogContext{
Path: r.RequestURI,
Method: r.Method,
Expand All @@ -107,7 +108,7 @@ func WithLog(next http.HandlerFunc) http.HandlerFunc {
logger.InfoC(reqContext, RequestLogMessage)
logger.DebugRequest(RequestLogMessage, r)
lw := &LoggableResponseWriter{Status: http.StatusOK, ResponseWriter: w}
lw.Header().Set(CorrelatorHTTPHeader, logContext.Correlator)
lw.Header().Set(CorrelatorHTTPHeader, logContext.GetCorrelator())
if isNewLogger {
next(lw, r.WithContext(context.WithValue(r.Context(), LoggerContextKey, logger)))
} else {
Expand Down Expand Up @@ -140,13 +141,15 @@ func WithNotFound() http.HandlerFunc {

// GetLogger to get the logger from the request context.
func GetLogger(r *http.Request) *Logger {
return r.Context().Value(LoggerContextKey).(*Logger)
logger, _ := r.Context().Value(LoggerContextKey).(*Logger)
return logger
}

// GetLogContext gets the log context associated to a request.
func GetLogContext(r *http.Request) *LogContext {
if logger := GetLogger(r); logger != nil {
return logger.GetLogContext().(*LogContext)
ctxt, _ := logger.GetLogContext().(*LogContext)
return ctxt
}
return nil
}
5 changes: 3 additions & 2 deletions mw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ func TestWithLog(t *testing.T) {
r := httptest.NewRequest("GET", "/users", nil)
r.Header.Add("Unica-Correlator", "corr")
var buf bytes.Buffer
var ctx LogContext
logger := newContextLogger(r, &ctx)
var ctxt LogContext
logger := NewLogger()
logger.SetLogContext(InitContext(r, &ctxt))
logger.out = &buf
r = r.WithContext(context.WithValue(r.Context(), LoggerContextKey, logger))
handler := func(w http.ResponseWriter, r *http.Request) {
Expand Down
38 changes: 38 additions & 0 deletions pipeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2018 Telefónica Investigación y Desarrollo, S.A.U
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package govice

import "net/http"

// Pipeline returns a HandlerFunc resulting of a list of middlewares and a final endpoint.
//
// The following example creates a pipeline of 2 middlewares:
//
// mws := []func(http.HandlerFunc) http.HandlerFunc{
// govice.WithLogContext(&logContext),
// govice.WithLog,
// }
// p := govice.Pipeline(mws, next)
//
func Pipeline(mws []func(http.HandlerFunc) http.HandlerFunc, endpoint http.HandlerFunc) http.HandlerFunc {
h := endpoint
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
Loading

0 comments on commit ec242c1

Please sign in to comment.