Go software measurement tools

Gopher with a magnifier


In this article, I would like to share a way to profile and trace programs on Go. I’ll show you how to do this while keeping the code flexible and clean.


TL; DR


Logging, collection of metrics and everything that is not connected with the main functionality of any code should not be inside this code. Instead, you need to identify trace points that can be used to measure code by the user.


In other words, logging and collection of metrics are subsets of the trace .


Template trace code can be generated using gtrace .


Problem


Suppose we have a package liband some structure lib.Client. Before executing any request, it lib.Clientchecks the connection:


package lib

type Client struct {
    conn net.Conn
}

func (c *Client) Request(ctx context.Context) error {
    if err := c.ping(ctx); err != nil {
        return err
    }
    // Some logic here.
}

func (c *Client) ping(ctx context.Context) error {
    return doPing(ctx, c.conn)
}

What if we want to record in the log right before and right after the ping message is sent? The first option is to embed the logger (or its interface) in Client:


package lib

type Client struct {
    Logger Logger

    conn net.Conn
}

func (c *Client) ping(ctx context.Context) (err error) {
    c.Logger.Info("ping started")
    err = doPing(ctx, c.conn)
    c.Logger.Info("ping done (err is %v)", err)
    return
}

If we want to collect any metrics, we can do the same:


package lib

type Client struct {
    Logger  Logger
    Metrics Registry

    conn net.Conn
}

func (c *Client) ping(ctx context.Context) (err error) {
    start := time.Now()
    c.Logger.Info("ping started")

    err = doPing(ctx, c.conn)

    c.Logger.Info("ping done (err is %v)", err)
    metric := c.Metrics.Get("ping_latency")
    metric.Send(time.Since(start))

    return err
}

, – . Client, , Γ³ , ( doPing()).


( Client) .


, , , ? , , ?


, Client.


, , - . , (SRP).


, Client ? , , ? , Go, , .


, :


,
.


, ( ), ( ).


, , .


, , httptrace Go.


, : OnPingStart() OnPingDone(), OnPing(), callback. OnPing() ping-, callback – . (, doPing()).


Client :


package lib

type Client struct {
    OnPing func() func(error)
    conn net.Conn
}

func (c *Client) ping(ctx context.Context) (err error) {
    done := c.OnPing()
    err = doPing(ctx, c.conn)
    done(err)
    return
}

, OnPing nil. :


func (c *Client) ping(ctx context.Context) (err error) {
    var done func(error)
    if fn := c.OnPing; fn != nil {
        done = fn()
    }
    err = doPing(ctx, c.conn)
    if done != nil {
        done(err)
    }
    return
}

SRP , .


, .



?
httptrace ClientTrace.compose(), n . n ( ).


( reflect). OnPing Client ClientTrace:


package lib

type Client struct {
    Trace ClientTrace
    conn net.Conn
}

type ClientTrace struct {
    OnPing func() func(error)
}

:


func (a ClientTrace) Compose(b ClientTrace) (c ClientTrace) {
    switch {
    case a.OnPing == nil:
        c.OnPing = b.OnPing
    case b.OnPing == nil:
        c.OnPing = a.OnPing
    default:
        c.OnPing = func() func(error) {
            doneA := a.OnPing()
            doneB := b.OnPing() 
            switch {
            case doneA == nil:
                return doneB
            case doneB == nil:
                return doneA
            default:
                return func(err error) {
                    doneA(err)
                    doneB(err)
                }
            }
        }
    }
    return c
}

, ? , .


:


package main

import (
    "log"

    "some/path/to/lib"
)

func main() {
    var trace lib.ClientTrace

    // Logging hooks.
    trace = trace.Compose(lib.ClientTrace{
        OnPing: func() func(error) {
            log.Println("ping start")
            return func(err error) {
                log.Println("ping done", err)
            }
        },
    })

    // Some metrics hooks.
    trace = trace.Compose(lib.ClientTrace{
        OnPing: func() func(error) {
            start := time.Now()
            return func(err error) {
                metric := stats.Get("ping_latency")
                metric.Send(time.Since(start))
            }
        },
    })

    c := lib.Client{
        Trace: trace,
    }
}


. ClientTrace context.Context, Client.Request():


package lib

type clientTraceContextKey struct{}

func ClientTrace(ctx context.Context) ClientTrace {
    t, _ := ctx.Value(clientTraceContextKey{})
    return t
}

func WithClientTrace(ctx context.Context, t ClientTrace) context.Context {
    prev := ContextClientTrace(ctx)
    return context.WithValue(ctx,
        clientTraceContextKey{},
        prev.Compose(t),
    )
}

. , !


?


, Vim ( - ), .


, , nil , reflection.


github.com/gobwas/gtrace


gtrace . , //gtrace:gen. - nil, .


.


:


package lib

//go:generate gtrace

//gtrace:gen
//gtrace:set context
type ClientTrace struct {
    OnPing func() func(error)
}

type Client struct {
    Trace ClientTrace
    conn net.Conn
}

func (c *Client) ping(ctx context.Context) (err error) {
    done := c.Trace.onPing(ctx)
    err = doPing(ctx, c.conn)
    done(err)
    return
}

go generate ClientTrace.


! gtrace , .


!


References



All Articles