ZhangZhihui's Blog  

 

 

In routes.go:

    router.Handler(http.MethodGet, "/debug/vars", expvar.Handler())

 

 

 

func main() {
    expvar.NewString("version").Set(version)
...

 

func main() {
...
    // Publish the version number.
    expvar.NewString("version").Set(version)

    // Publish the number of active goroutines.
    expvar.Publish("goroutines", expvar.Func(func() any {
        return runtime.NumGoroutine()
    }))

    // Publish the database connection pool statistics.
    expvar.Publish("database", expvar.Func(func() any {
        return poolWrapper
    }))

    // Publish the current Unix timestamp.
    expvar.Publish("timestamp", expvar.Func(func() any {
        return time.Now().Unix()
    }))

    // Create the application instance.
    app := &application{
        config:      cfg,
        logger:      logger,
        models:      data.NewModels(&poolWrapper),
        emailSender: &mail.EmailSender{SMTPCfg: cfg.smtp},
    }
...

 

internal/data/db.go:

package data

import (
    "context"
    "encoding/json"
    "time"

    "github.com/jackc/pgx/v5/pgxpool"
)

// PoolWrapper wraps a *pgxpool.Pool.
type PoolWrapper struct {
    Pool *pgxpool.Pool `json:"-"`
    Stat struct {
        PoolSerialNumber        int32         `json:"pool_serial_number"`      // serial number of the pool in use
        AcquireCount            int64         `json:"AcquireCount"`            // cumulative count of successful acquires from the pool
        AcquireDuration         time.Duration `json:"AcquireDuration"`         // total duration of all successful acquires from the pool
        AcquiredConns           int32         `json:"AcquiredConns"`           // number of currently acquired connections in the pool
        CanceledAcquireCount    int64         `json:"CanceledAcquireCount"`    // cumulative count of acquires from the pool that were canceled by a context
        EmptyAcquireCount       int64         `json:"EmptyAcquireCount"`       // cumulative count of successful acquires from the pool that waited for a resource to be released or constructed because the pool was empty
        IdleConns               int32         `json:"IdleConns"`               // number of currently idle conns in the pool
        MaxConns                int32         `json:"MaxConns"`                // maximum size of the pool
        TotalConns              int32         `json:"TotalConns"`              // total number of resources currently in the pool, the sum of ConstructingConns, AcquiredConns, and IdleConns
        NewConnsCount           int64         `json:"NewConnsCount"`           // cumulative count of new connections opened
        MaxLifetimeDestroyCount int64         `json:"MaxLifetimeDestroyCount"` // cumulative count of connections destroyed because they exceeded MaxConnLifetime
        MaxIdleDestroyCount     int64         `json:"MaxIdleDestroyCount"`     // cumulative count of connections destroyed because they exceeded MaxConnIdleTime
    }
}

// Implement the MarshalJSON method on PoolWrapper struct so that it satisfies the jons.Marshaler interface.
func (pw *PoolWrapper) MarshalJSON() ([]byte, error) {
    return json.Marshal(pw.Stat)
}

// CreatePool creates a *pgxpool.Pool and assigns it to the wrapper's Pool field.
func (pw *PoolWrapper) CreatePool(connString string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    p, err := pgxpool.New(ctx, connString)
    if err != nil {
        return err
    }

    err = p.Ping(ctx)
    if err != nil {
        p.Close()
        return err
    }

    pw.Pool = p
    pw.Stat.PoolSerialNumber = pw.Stat.PoolSerialNumber + 1
    pw.Stat.AcquireCount = p.Stat().AcquireCount()
    pw.Stat.AcquireDuration = p.Stat().AcquireDuration()
    pw.Stat.AcquiredConns = p.Stat().AcquiredConns()
    pw.Stat.CanceledAcquireCount = p.Stat().CanceledAcquireCount()
    pw.Stat.EmptyAcquireCount = p.Stat().EmptyAcquireCount()
    pw.Stat.IdleConns = p.Stat().IdleConns()
    pw.Stat.MaxConns = p.Stat().MaxConns()
    pw.Stat.TotalConns = p.Stat().TotalConns()
    pw.Stat.NewConnsCount = p.Stat().NewConnsCount()
    pw.Stat.MaxLifetimeDestroyCount = p.Stat().MaxLifetimeDestroyCount()
    pw.Stat.MaxIdleDestroyCount = p.Stat().MaxIdleDestroyCount()

    return nil
}

 

Note that the anonymous functions in the expvar.Publish statements are run only when the /debug/vars endpoint is visited.

    // Publish the number of active goroutines.
    expvar.Publish("goroutines", expvar.Func(func() any {
        return runtime.NumGoroutine()
    }))

    // Publish the database connection pool statistics.
    expvar.Publish("database", expvar.Func(func() any {
        return poolWrapper
    }))

    // Publish the current Unix timestamp.
    expvar.Publish("timestamp", expvar.Func(func() any {
        return time.Now().Unix()
    }))

 

func (app *application) metrics(next http.Handler) http.Handler {
    var (
        totalRequestsReceived           = expvar.NewInt("total_requests_received")
        totalResponsesSent              = expvar.NewInt("total_responses_sent")
        totalProcessingTimeMicroseconds = expvar.NewInt("total_processing_time_μs")
    )

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        totalRequestsReceived.Add(1)

        next.ServeHTTP(w, r)

        totalResponsesSent.Add(1)

        duration := time.Since(start).Microseconds()
        totalProcessingTimeMicroseconds.Add(duration)
    })
}

In routes.go:

    return app.metrics(app.recoverPanic(app.enableCORS(app.rateLimit(app.authenticate(router)))))

 

 

zzh@ZZHPC:~$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
zzh@ZZHPC:~$ hey -d "$BODY" -m "POST" http://localhost:4000/v1/tokens/authentication

Summary:
  Total:	6.9818 secs
  Slowest:	2.0896 secs
  Fastest:	1.0939 secs
  Average:	1.6953 secs
  Requests/sec:	28.6458
  
  Total data:	28581 bytes
  Size/request:	142 bytes

Response time histogram:
  1.094 [1]	|■
  1.193 [0]	|
  1.293 [2]	|■■
  1.393 [2]	|■■
  1.492 [20]	|■■■■■■■■■■■■■■■
  1.592 [25]	|■■■■■■■■■■■■■■■■■■■
  1.691 [40]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.791 [53]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.890 [39]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.990 [14]	|■■■■■■■■■■■
  2.090 [4]	|■■■


Latency distribution:
  10% in 1.4837 secs
  25% in 1.5957 secs
  50% in 1.7114 secs
  75% in 1.8080 secs
  90% in 1.8827 secs
  95% in 1.9387 secs
  99% in 2.0885 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0003 secs, 1.0939 secs, 2.0896 secs
  DNS-lookup:	0.0001 secs, 0.0000 secs, 0.0011 secs
  req write:	0.0001 secs, 0.0000 secs, 0.0010 secs
  resp wait:	1.6949 secs, 1.0938 secs, 2.0884 secs
  resp read:	0.0000 secs, 0.0000 secs, 0.0001 secs

Status code distribution:
  [201]	200 responses

Once that has completed, if you refresh localhost:4000/debug/vars in your browser your metrics should now look something like this:

 

In middleware.go:

// The metricsResponseWriter type wraps an existing http.ResponseWriter and also
// contains a field for recording the response status code, and a boolen flag
// to indicate whether the response headers have already been written.
type metricsResponseWriter struct {
    wrapped       http.ResponseWriter
    statusCode    int
    headerWritten bool
}

func newMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter {
    return &metricsResponseWriter{
        wrapped:    w,
        statusCode: http.StatusOK,
    }
}

// Header is a simple 'pass through' to the Header() method of the wrapped 
// http.ResponseWriter.
func (mrw *metricsResponseWriter) Header() http.Header {
    return mrw.wrapped.Header()
}

// WriteHeader does a 'pass through' to the WriteHeader() method of the wrapped
// http.ResponseWriter. But after this returns, we also record the response status
// code (if it hasn't already been recorded) and set the headerWritten field to
// true to indicate that the HTTP response headers have now been written.
func (mrw *metricsResponseWriter) WriteHeader(statusCode int) {
    mrw.wrapped.WriteHeader(statusCode)

    if !mrw.headerWritten {
        mrw.statusCode = statusCode
        mrw.headerWritten = true
    }
}

// Write does a 'pass through' to the Write() method of the wrapped http.ResponseWriter.
// Calling this will automatically write any response headers, so we set the
// headerWritten field to true.
func (mrw *metricsResponseWriter) Write(b []byte) (int, error) {
    mrw.headerWritten = true
    return mrw.wrapped.Write(b)
}

// Unwrap returns the existing wrapped http.ResponseWriter.
func (mrw *metricsResponseWriter) Unwrap() http.ResponseWriter {
    return mrw.wrapped
}

 

 

func (app *application) metrics(next http.Handler) http.Handler {
    var (
        totalRequestsReceived           = expvar.NewInt("total_requests_received")
        totalResponsesSent              = expvar.NewInt("total_responses_sent")
        totalProcessingTimeMicroseconds = expvar.NewInt("total_processing_time_μs")
        totalResponsesSentByStatus      = expvar.NewMap("total_responses_sent_by_status")
    )

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        totalRequestsReceived.Add(1)

        mrw := newMetricsResponseWriter(w)

        next.ServeHTTP(mrw, r)

        totalResponsesSent.Add(1)

        totalResponsesSentByStatus.Add(strconv.Itoa(mrw.statusCode), 1)

        duration := time.Since(start).Microseconds()
        totalProcessingTimeMicroseconds.Add(duration)
    })
}

 

zzh@ZZHPC:~$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
zzh@ZZHPC:~$ hey -d "$BODY" -m "POST" http://localhost:4000/v1/tokens/authentication

Summary:
  Total:	7.0053 secs
  Slowest:	2.3455 secs
  Fastest:	1.0091 secs
  Average:	1.7081 secs
  Requests/sec:	28.5500
  
  Total data:	28577 bytes
  Size/request:	142 bytes

Response time histogram:
  1.009 [1]	|■
  1.143 [1]	|■
  1.276 [2]	|■
  1.410 [11]	|■■■■■■■
  1.544 [19]	|■■■■■■■■■■■■
  1.677 [45]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.811 [63]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.945 [39]	|■■■■■■■■■■■■■■■■■■■■■■■■■
  2.078 [15]	|■■■■■■■■■■
  2.212 [3]	|■■
  2.346 [1]	|■


Latency distribution:
  10% in 1.4508 secs
  25% in 1.5917 secs
  50% in 1.7303 secs
  75% in 1.8421 secs
  90% in 1.9413 secs
  95% in 1.9940 secs
  99% in 2.1478 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0003 secs, 1.0091 secs, 2.3455 secs
  DNS-lookup:	0.0002 secs, 0.0000 secs, 0.0011 secs
  req write:	0.0001 secs, 0.0000 secs, 0.0010 secs
  resp wait:	1.7076 secs, 1.0091 secs, 2.3455 secs
  resp read:	0.0000 secs, 0.0000 secs, 0.0001 secs

Status code distribution:
  [201]	200 responses

 

 

posted on 2024-11-28 21:22  ZhangZhihuiAAA  阅读(10)  评论(0编辑  收藏  举报