goji+xorm: Building web api server in golang (part1)

Hello gophers

Hey guys, this is the article of eureka advent calendar day 20th.
Today, I’m writing about crash course for building golang web server.

What’s APP?

I prefer special applications like “Earthworm Jail” or “Horrible Nightmare Alert System”, etc…
But don’t worry, we’ll just create traditional Blog in this article against my wish :=)

What to use?

For this go app, use other libraries below,

Ch.0: Prepare

Before write the code, we have to do some routine things.

Ch.0-0: Install go

I believe you gophers already installed.
But, oh gosh, you didn’t? Then you go here.

Ch.0-1 Initialize repository

To initialized the repository for the blog, it’s not special.

$ mkdir /path/to/app
$ cd /path/to/app
$ git init

Ch.0-2 Setting pre-commit

I strongly recommend setting pre-commit at this time and use same one for the team.
So we do not need worry about syntax or other some silly things.

$ cd /path/to/app
$ mkdir misc
$ vim misc/pre-commit
$ chmod +x misc/pre-commit

And use below script.
This script checks below for commit-staging files

  • gofmt
  • golint
  • go vet
##!/bin/bash

##===================
##  setting
##===================

REPO_ROOT=`git rev-parse --show-toplevel`
GOLINT_TMP="/tmp/.golint.txt"

GVM_INIT=$HOME/.gvm/scripts/gvm
GVM_VERSION="stable"


if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

exec 1>&2

##===================
##  function
##===================

## set env for github desktop app
function initEnv() {
  PATH="$PATH:/usr/local/bin"
  which gvm > /dev/null || (test -s $GVM_INIT && source $GVM_INIT)
}

## check the file is golang file or not
function isGoFile() {
  echo "$1" | egrep '\.go$' > /dev/null
}

## check golint is installed or not
function isGolintInstalled() {
  which golint > /dev/null || { gvm use $GVM_VERSION > /dev/null && which golint > /dev/null; }
}

##===================
##  main routine
##===================

IS_ERROR=0
FILES=`git diff-index --name-status $against --`
ADD_FILES=`echo -n "$FILES"| egrep '^(A|M)'  | cut -c3-`

initEnv

isGolintInstalled
if [ "$?" -ne 0 ]; then
  echo "[ERROR] golint is not installed"
  echo "--- type below code to install ---"
  echo "$ go get github.com/golang/lint/golint"
  echo ""
  IS_ERROR=1
  exit $IS_ERROR
fi

for FILE in `echo -n "$ADD_FILES" `; do
  isGoFile $FILE
  if [ "$?" -eq 0 ]; then
    echo "[$FILE]"
    gofmt -s -w $FILE
    if [ "$?" -ne 0 ]; then
      IS_ERROR=2
    fi

    go vet $FILE
    if [ "$?" -ne 0 ]; then
      IS_ERROR=3
    fi

    # golint always return 0, so use temporary file
    golint $FILE | tee -a $GOLINT_TMP

    echo ""
    continue
  fi
done

if [ -s "$GOLINT_TMP" ]; then
  rm $GOLINT_TMP 
  IS_ERROR=4
fi

## check result
case "$IS_ERROR" in
  2)
    echo "!!!!"
    echo "!! [Go fmt Error] Fix above gofmt errors"
    ;;
  3)
    echo "!!!!"
    echo "!! [Go vet Error] Fix above vet errors"
    ;;
  4)
    echo "!!!!"
    echo "!! [Go LINT Error] Fix above lint errors"
    ;;
  *)
    echo "[Success] no errors detected"
    ;;
esac

exit $IS_ERROR

and set this for your local repo.

$ cd /path/to/app
$ cd .git/hooks/
$ ln -s ../../misc/pre-commit ./pre-commit

Don't forget to force to do this for all of your team members.
Otherwise your efforts thrown into chaos… (p_q)

<h2>Ch.1 HTTP Server</h2>

We'll build http server in this chapter.

<h3>Ch.1-1 Plain Server</h3>

We'll start with plain http server for the simple illustration.

Save this code as <code>/path/to/app/main.go</code>

// main.go
package main

import (
    "flag"

    "github.com/zenazn/goji"
)

func main() {
    flag.Set("bind", ":1234")

    // run http server
    goji.Serve()
}

And get dependencies

$ go get -v ./...

Then run go!

## terminal 1
$ go run main.go

## terminal 2
$ curl localhost:1234

404 page not found

You can see access logs on terminal1.
So easy, right?

If you want to change listen port, use <code>-bind</code> flag.

## terminal 1, don't forget to put colon before port number
$ go run main.go -bind :8080

## terminal 2
$ curl localhost:8080

Okay, this is just dummy server like a scarecrow, so we have to add some features.

<h3>Ch.1-2 Routings</h3>

To change response for each endpoint, set routing.

// main.go
package main

import (
    "flag"

    "github.com/zenazn/goji"
    "github.com/zenazn/goji/web"
    "github.com/zenazn/goji/web/middleware"

    // you have to change here to fit your gopath
    "github.com/eure/example-blog-golang/routing"
)

func main() {
    flag.Set("bind", ":1234")

    // api v1 routing
    routeV1 := web.New()
    routeV1.Use(middleware.SubRouter)
    goji.Handle("/api/v1/*", routeV1)
    routing.SetV1(routeV1)

    // run http server
    goji.Serve()
}

In this addition, when client access to <code>http:///api/v1/…</code>, custom routing is used.

Define custom routing,

// routing/v1.go
package routing

import (
    "fmt"
    "net/http"

    "github.com/zenazn/goji/web"
)

// SetV1 sets api routing ver1
func SetV1(r *web.Mux) {
    r.Get("/foo", func(c web.C, w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "custom route!")
    })
}

Here, add simple echo process for <code>GET /api/v1/foo</code>.

goji's <code>web.Mux</code> has method for set routing.
<code>Get</code>, <code>Post</code>, <code>Put</code>, <code>Delete</code>, correspond to typical HTTP method. (<a href="https://godoc.org/github.com/zenazn/goji/web">see doc for other method</a>)
1st parameter <code>/foo</code> is for the path the client accessed, and 2nd is function you want to do.
In this routing, we used <code>middleware.SubRouter</code> and set custom handler for <code>/api/v1/
</code>, so <code>/api/v1/foo</code> is mapped here.

Check the behavior.

$ go run main.go

## custom route!
$ curl localhost:1234/api/v1/foo

## 404 page not found
$ curl localhost:1234/api/v1/

See that?

<h3>Ch.1-3 Controller</h3>

It's handy to write all logic same place, but easily get unmaintainable.
So move logic to other layers.

For the set of routes and web-related(request and response in this context) logic, we used the layer called <code>controller</code>.

But before that, we need json response.

Create response helper.

// controller/json_response.go
package controller

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

// status codes
const (
    StatusOK      = http.StatusOK
    StatusCreated = http.StatusCreated
)

// JSONResponse is alias of map for JSON response
type JSONResponse struct {
    data   map[string]interface{}
    status int
}

// NewResponse creates a new JSONResponse
func NewResponse() *JSONResponse {
    r := &JSONResponse{
        data:   make(map[string]interface{}),
        status: StatusOK,
    }
    return r
}

// Merge adds multiple map data to the response
func (r *JSONResponse) Merge(data map[string]interface{}) {
    for k, v := range data {
        r.data[k] = v
    }
}

// Add adds a single key-value to the response
func (r *JSONResponse) Add(key string, value interface{}) {
    r.data[key] = value
}

// SetCreated set http status code to 201
func (r *JSONResponse) SetCreated() {
    r.status = StatusCreated
}

// RenderJSON render json response
func RenderJSON(w http.ResponseWriter, msg interface{}) {
    switch v := msg.(type) {
    case *JSONResponse:
        if _, ok := v.data["error"]; !ok {
            v.data["error"] = nil
        }
        w.WriteHeader(v.status)
        msg = v.data
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(msg)
}

<code>JSONResponse</code> is struct for JSON response. This has two fields, data and status.
As named, data is for response and status is for http status code.
To initialize the response, use <code>NewResponse</code>.
And pass it to <code>RenderJSON</code>, output the data and status code with http header for JSON.

Then create controller for authors.

// controller/apiv1/author_controller.go
package apiv1

import (
    "net/http"

    "github.com/zenazn/goji/web"

    "github.com/eure/example-blog-golang/controller"
)

// GetAuthor shows author data
func GetAuthor(c web.C, w http.ResponseWriter, r *http.Request) {
    data := controller.NewResponse()
    data.Add("object", "author")
    data.Add("name", c.URLParams["name"])
    controller.RenderJSON(w, data)
}

<code>c.URLParams["name"]</code> is from routing. See below,

// routing/v1.go
package routing

import (
    "github.com/zenazn/goji/web"

    "github.com/eure/example-blog-golang/controller/apiv1"
)

// SetV1 sets api routing ver1
func SetV1(r *web.Mux) {
    r.Get("/author/:name", apiv1.GetAuthor)
}

See <code>SetV1</code>, The path is <code>/author/:name</code>, <code>:name</code> can be anything the client send, and saved to map data in goji's <code>web.C.URLParams</code> as string data.

Let's check again,

$ curl -s localhost:1234/api/v1/author/takuma
{"error":null,"name":"takuma","object":"author"}

$ curl -s localhost:1234/api/v1/author/your_sweetheart
{"error":null,"name":"your_sweetheart","object":"author"}

Yay!

<h3>Ch.1-4 Context</h3>

Context is used for passing request-local data in this app.
Without passing many parameter, context carries everything you need.
<a href="http://blog.golang.org/context">see detailed illustration about context</a>

First, create context on middleware layer.

// framework/middleware/context.go
package middleware

import (
    "net/http"

    "github.com/zenazn/goji/web"
    "golang.org/x/net/context"
)

// ContextKey is key name for stored context
const ContextKey = "context"

// Context creates new Context for new request
func Context(c *web.C, h http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        ctx := context.Background()
        ctx = context.WithValue(ctx, "request", r)
        c.Env[ContextKey] = ctx
        h.ServeHTTP(w, r)
    }
    return http.HandlerFunc(fn)
}

And use this middleware for each request, Add it to main.go

// main.go
package main

import (
    "flag"

    "github.com/zenazn/goji"
    "github.com/zenazn/goji/web"
    "github.com/zenazn/goji/web/middleware"

    mw "github.com/eure/example-blog-golang/framework/middleware" // <- set alias to avoid name conflict
    "github.com/eure/example-blog-golang/routing"
)

func main() {
    flag.Set("bind", ":1234")

    // api v1 routing
    routeV1 := web.New()
    routeV1.Use(mw.Context) // <- create context
    routeV1.Use(middleware.SubRouter)
    goji.Handle("/api/v1/*", routeV1)
    routing.SetV1(routeV1)

    // run http server
    goji.Serve()
}

So now, context is saved in <code>c.Env["context"]</code>.
Create helper for this,

// controller/context.go
package controller

import (
    "github.com/zenazn/goji/web"
    "golang.org/x/net/context"

    "github.com/eure/example-blog-golang/framework/middleware"
)

// GetContext returns context
func GetContext(c web.C) context.Context {
    if ctx, ok := c.Env[middleware.ContextKey]; ok {
        return ctx.(context.Context)
    }
    panic("context missing!!")
}

Then, use it on the controller.

// controller/apiv1/author_controller.go

func GetAuthor(c web.C, w http.ResponseWriter, r *http.Request) {
    data := controller.NewResponse()
    data.Add("object", "author")
    data.Add("name", c.URLParams["name"])

    ctx := controller.GetContext(c)
    if ctx != nil {
        r2 := ctx.Value("request").(*http.Request)
        data.Add("is_same_request", r == r2)
    }
    controller.RenderJSON(w, data)
}

Then check this, you can see the request is saved in the context and easily extract it.

<h2>Ch.2 Database</h2>

In this chapter, we accomplish database access.

<h3>Ch.2-1 Initialize engine(database resource)</h3>

<code>Engine</code> is wrapper struct of database recource <code>*sql.DB</code>.
We create it just once, and use it every time.

// library/database/engine.go
package database

import (
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql" // load mysql driver for xorm
    "github.com/go-xorm/core"
    "github.com/go-xorm/xorm"
)

var engine *xorm.Engine

func init() {
    loc, err := time.LoadLocation("UTC")
    if err != nil {
        panic(err)
    }
    time.Local = loc
}

// UseEngine reutrns db resource
func UseEngine() *xorm.Engine {
    if engine == nil {
        engine = initEngine()
    }
    return engine
}

func initEngine() *xorm.Engine {
    dbUser := "user"
    dbPass := "pass"
    dbHost := "127.0.0.1"
    dbPort := 3306
    dbName := "blogdb"
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbUser, dbPass, dbHost, dbPort, dbName)

    e, err := xorm.NewEngine("mysql", dsn)
    if err != nil {
        panic(err)
    }

    // as default mapper, underscore is inserted on every capital case for table or field name.
    // (e.g.) UserID => user_i_d, IPAddress => i_p_address
    // to prevent this rule, use GonicMapper.
    e.SetMapper(core.NewCacheMapper(new(core.GonicMapper)))

    err = e.Ping()
    if err != nil {
        panic(err)
    }
    return e
}

go provides connection pooling as default. so save Engine on global scope.
And set UTC for application timezone, to keep same timezone for every process.

<h3>Ch.2-2 Save engine to context</h3>

To carry same session or transaction flexibly, we must save it to right place.
To begin with that, we'll store Engine into context.

// library/net/context/resource/resource_database.go
package resource

import (
    "github.com/go-xorm/xorm"
    "golang.org/x/net/context"

    "github.com/eure/example-blog-golang/library/database"
)

// GetDB returns database engine
func GetDB(c context.Context) *xorm.Engine {
    v, ok := c.Value("database").(*xorm.Engine)
    if !ok {
        return nil
    }
    return v
}

// UseDB returns database engine
func UseDB(c context.Context) *xorm.Engine {
    db := GetDB(c)
    if db == nil {
        db = database.UseEngine()
        c = context.WithValue(c, "database", db)
    }
    return db
}

// UseTx returns database transaction
func UseTx(c context.Context) *xorm.Session {
    tx := c.Value("transaction").(*xorm.Session)
    if tx == nil {
        db := UseDB(c)
        tx := db.NewSession()
        tx.Begin()
        c = context.WithValue(c, "transaction", tx)
    }
    return tx
}

// Commit commits database transaction
func Commit(c context.Context) error {
    tx := c.Value("transaction").(*xorm.Session)
    if tx == nil {
        return nil
    }

    err := tx.Commit()
    if err != nil {
        return err
    }

    c = context.WithValue(c, "transaction", nil)
    tx.Close()
    return nil
}

// Rollback rollbacks database transaction
func Rollback(c context.Context) error {
    tx := c.Value("transaction").(*xorm.Session)
    if tx == nil {
        return nil
    }

    err := tx.Rollback()
    if err != nil {
        return err
    }

    c = context.WithValue(c, "transaction", nil)
    tx.Close()
    return nil
}

// Release releases all resources
func Release(c context.Context) {
    if c == nil {
        return
    }

    tx := c.Value("transaction").(*xorm.Session)
    if tx != nil {
        tx.Close()
    }
}

These functions are for database access.
<code>UseDB</code> behaves like <code>get or create</code>, if db resource is already created then return it,
Otherwise create new one and return it.

You don't have to save db resource into context at this time.
But definitly should do it when you plan to partisioning or sharding or read-only account or something like that.

<h2>Ch.3 Model</h2>

In this app, model is used for persistent data like the data in database or cache.
model has two different concepts.

<ul>
<li>entity: the things contains data. mostly correspond to database row.(e.g. author, article, tag)</li>
<li>repository: handle entity. fetch and save entity.</li>
</ul>

<h3>Ch. 3-1 Author model</h3>

Author has name and email. Even my dad has both,
So the sophisticated authors must have them!
(but my grandmother does not have the later)

// model/author/author_entity.go
package author

import (
    "fmt"
)

// Entity is struct for author
type Entity struct {
    Email string `response:"email" xorm:"pk not null unique"`
    Name  string `response:"name" xorm:"not null unique"`
}

const (
    tableName = "authors"
    pkName    = "email"
)

// TableName define table name for Xorm
func (e Entity) TableName() string {
    return tableName
}

func (e Entity) String() string {
    return fmt.Sprintf("{email: %s, name: %s}", e.Email, e.Name)
}

<code>TableName()</code> is used as database table name for xorm, and <code>String()</code> is for print data.

<h3>Ch. 3-2 Root repository</h3>

Before we write author's repository, create the base repository as a parent class called <code>root repository</code>.
This utilizes context functions for the resource.

// model/root_repository.go
package model

import (
    "fmt"

    "github.com/go-xorm/xorm"
    "golang.org/x/net/context"

    "github.com/eure/example-blog-golang/library/net/context/resource"
)

// RootRepository is root struct for all repositories
type RootRepository struct {
    Ctx    context.Context
    Engine *xorm.Engine

    PrimaryKey string
    Seed       interface{}
    omitList   []string
}

// NewRootRepositoryWithSeed creates new RootRepository
func NewRootRepositoryWithSeed(ctx context.Context, pk string, seed interface{}) *RootRepository {
    return &RootRepository{
        Ctx:        ctx,
        Engine:     resource.UseDB(ctx),
        PrimaryKey: pk,
        Seed:       seed,
    }
}

// ============================================================
//  helper
// ============================================================

// NewSession return session
func (r *RootRepository) NewSession() *xorm.Session {
    return r.Engine.NewSession()
}

// AddOmit adds column name to omit list
func (r *RootRepository) AddOmit(col string) {
    r.omitList = append(r.omitList, col)
}

// GetLastSQL returns the executed SQL statement
func (r *RootRepository) GetLastSQL(s *xorm.Session) string {
    sql, args := s.LastSQL()
    return fmt.Sprintf("%s -- [args] %v", sql, args)
}

// ============================================================
//  create
// ============================================================

// CreateOne inserts new entity data to database
func (r *RootRepository) CreateOne(e interface{}) error {
    s := r.Engine.NewSession()
    if len(r.omitList) > 0 {
        for _, col := range r.omitList {
            s.Omit(col)
        }
        r.omitList = []string{}
    }

    _, err := s.Insert(e)
    if err != nil {
        return err
    }
    return nil
}

// ============================================================
//  get
// ============================================================

// GetOneByPK fetches a single row by primary key
func (r *RootRepository) GetOneByPK(ent, pk interface{}) (bool, error) {
    s := r.Engine.NewSession()
    s.And(r.PrimaryKey+" = ?", pk)
    return r.GetOneBySession(s, ent)
}

// GetOneBySession fetches a single row using the given session
func (r *RootRepository) GetOneBySession(s *xorm.Session, ent interface{}) (bool, error) {
    has, err := s.Get(ent)
    if err != nil {
        return has, err
    }
    return has, nil
}

<code>RootRepository</code> has five fields. Don't need to explain about <code>Ctx</code> and <code>Engine</code> .
Rest of three helps to reduce template coding.

<ul>
<li><code>PrimaryKey</code> is a column name of primary key, which is used by <code>GetOneByPK</code></li>
<li><code>Seed</code> is an entity. It's useful for the process without row data.(e.g. count, existence)</li>
<li><code>omitList</code> is used for <code>INSERT</code>. When you want to use default value of the table, you must use <code>Omit</code> not to be filled by zero-value.(e.g. <code>default null</code> vs <code>0</code>, <code>""</code>, <code>0000-00-00</code>)</li>
</ul>

Okay, forget it now. Go to next chapter.

<h3>Ch. 3-3 Author repository</h3>

So now, we can create author's repository with embedding root repository.

//  model/author/author_repository.go
package author

import (
    "golang.org/x/net/context"

    "github.com/eure/example-blog-golang/model"
)

// Repository is repo for author
type Repository struct {
    *model.RootRepository
}

// NewRepository creates new Repository
func NewRepository(ctx context.Context) *Repository {
    return &Repository{
        RootRepository: model.NewRootRepositoryWithSeed(ctx, pkName, new(Entity)),
    }
}


// GetByEmail fetches a single author by email
func (r *Repository) GetByEmail(email string) *Entity {
    ent := new(Entity)
    has, err := r.GetOneByPK(ent, email)
    switch {
    case err != nil:
        // TODO: logging
        return nil
    case !has:
        return nil
    }
    return ent
}

This repository has just a method for getting author from authors table.
Executed SQL gonna be <code>SELECT email, name FROM authors WHERE email = ?</code>.

<h2>Ch.4 Logging</h2>

Oh, we forgot to write logs. This is really important thing to run any service.
If you create system without it, you cannot sleep whole 1 week (or more…(/_;) ) when get troubled…

<h3>Ch. 4-1 Debug prints</h3>

These are just for debug prints to stdout.
This simple technology could help you a lot of development time.

// library/log/debug.go
package log

import (
    wrapper "github.com/evalphobia/go-log-wrapper/log"
)

// Nothing is dummy variable for import error
var Nothing int

// Dump prints dump variable in console
func Dump(v interface{}) {
    wrapper.Dump(v)
}

// Print prints variable information in console
func Print(v interface{}) {
    wrapper.Print(v)
}

// Header prints separator in console
func Header(v ...interface{}) {
    wrapper.Header(v...)
}

// Mark prints trace info
func Mark() {
    wrapper.Mark(3)
}

I got all of logic from <a href="https://github.com/evalphobia/go-log-wrapper/blob/master/log/log.go">here</a>.

<ul>
<li><code>Dump</code>: just a wrapper of <a href="https://github.com/davecgh/go-spew">spew.Dump</a></li>
<li><code>Print</code>: wrapper of <code>fmt.Printf("%#v", value)</code></li>
<li><code>Header</code>: echo "==============================" for the readability.</li>
<li><code>Mark</code>: print filename and row number.</li>
</ul>

And Nothing is used for avoiding import check.

<br />import "github.com/eure/example-blog-golang/library/log"

// you don't have to add/delete import for logging to put it
_ = log.Nothing

<h3>Ch.4-2 logrus</h3>

<a href="https://github.com/Sirupsen/logrus">logrus</a> is cool logger.
it enables flexible logging and hooks to other systems.
I created a wrapper of logrus, to restrict flexibility but adds some tracing.

Add this first,

// library/log/log.go
package log

import (
    "github.com/Sirupsen/logrus"
    wrapper "github.com/evalphobia/go-log-wrapper/log"
    "golang.org/x/net/context"

    "github.com/eure/example-blog-golang/library/net/context/resource"
)

const (
    tagPrefix = "blog."
)

// init sets Sentry configurations
func init() {
    logrus.SetLevel(logrus.DebugLevel)
}

// Packet is wrapper struct
type Packet wrapper.Packet

// Panic logs panic error
func (p Packet) Panic(v ...interface{}) {
    p = p.setOptionalData(v)
    p.setDefaultTag("panic")
    go (wrapper.Packet(p)).Panic()
}

// Error logs serious error
func (p Packet) Error(v ...interface{}) {
    p = p.setOptionalData(v)
    p.setDefaultTag("error")
    go (wrapper.Packet(p)).Error()
}

// Warn logs warning
func (p Packet) Warn(v ...interface{}) {
    p = p.setOptionalData(v)
    p.setDefaultTag("warn")
    go (wrapper.Packet(p)).Warn()
}

// Info logs information
func (p Packet) Info(v ...interface{}) {
    p = p.setOptionalData(v)
    p.setDefaultTag("info")
    go (wrapper.Packet(p)).Info()
}

// Debug logs development information
func (p Packet) Debug(v ...interface{}) {
    p = p.setOptionalData(v)
    p.setDefaultTag("debug")
    go (wrapper.Packet(p)).Debug()
}

// setOptionalData adds trace data
func (p Packet) setOptionalData(v interface{}) Packet {
    // set trace
    if p.TraceData == nil {
        if p.Trace == 0 {
            p.Trace = 20
        }
        if p.TraceSkip == 0 {
            p.TraceSkip = 4
        }
        p.TraceData = wrapper.GetTraces(p.Trace, p.TraceSkip)
    }

    // set *http.Request from context
    if ctx, ok := v[0].(context.Context); ok {
        p.Request = resource.GetRequest(ctx)
    }
    return p
}

// AddData adds logging data
func (p *Packet) AddData(v ...interface{}) *Packet {
    p.DataList = append(p.DataList, v...)
    return p
}

func (p *Packet) setDefaultTag(tag string) {
    if p.Tag != "" {
        return
    }
    p.Tag = tagPrefix + tag
}

And use like below,

import "github.com/eure/example-blog-golang/library/log"

log.Packet{
    Title: "logging summary", 
    Data: nil,              // anything you want to log
    Err: err,               // error struct
    Request: *http.Request, // it's really helpful for send data to sentry 
    NoTrace: true,          // When set this, skip to log stack trace  (default=false)
}.Info(ctx)                 // automatically set *http.Request to Request when context passed

<h3>Ch. 4-3 Hooks</h3>

I personally used fluentd and Sentry for logging sub system.
fluentd is used for every logs, and sent it to other system. (e.g. S3, ElasticSearch, GoogleBigQuery etc…)
Sentry is for error tracking.

Are you interested in these? really easy to use!
Just like this,

import (
    "github.com/Sirupsen/logrus"
    "github.com/evalphobia/go-log-wrapper/log/fluent"
    "github.com/evalphobia/go-log-wrapper/log/sentry"
)

// SetFluentHook sets a logging hook for fluentd
func SetFluentHook() {
    fluent.AddLevel(logrus.DebugLevel) // logged above debug level
    fluent.Set("127.0.0.1", 24224)
}

// SetSentryHook sets a logging hook for sentry
func SetSentryHook() {
    sentry.AddLevel(logrus.WarnLevel) // logged above warn level
    sentry.Set("https://xxx:yyy@app.getsentry.com/99999")
}

And you set these hooks, then send logs to the other system every time you call log method.

What’s the next part…

We finished today’s part. Did you have fun?

We created just a bare structure for that.
But we are still on the way to making a cool blog system…

So the next part will include…

  • configs
  • logics
  • error handlings
  • unit testing
  • continuous integrations

I don’t think all of above I wrote is best or suitable for every situation,
if you have any suggestion or cool advice, please tell me.

You can see the whole codes here.

See ya!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

これだけ押さえておけば大丈夫!Webサービス向けVPCネットワークの設計指針

DIを使ってAndroidでイイ感じにテストを書く