gorilla/mux framework (rk-boot): add API logging middleware

introduce

Add API logging middleware to gorilla/mux microservices through a complete example .

What is a log interceptor/middleware?

The log interceptor will log every API request.

We will use rk-boot to start the gorilla/mux microservice .

Please visit the following address for the full tutorial: https://github.com/rookie-ninja/rk-mux

Install

go get github.com/rookie-ninja/rk-boot/mux

quick start

rk-boot integrates the following three open source libraries by default.

  • uber-go/zap is used as the underlying logging library.
  • logrus as log rolling.
  • loki is stored remotely as a log.

1. Create boot.yaml

The boot.yaml file describes the original information of GoFrame framework startup, and rk-boot starts gorilla/mux by reading boot.yaml .

To verify, we started the commonService at the same time. CommonService contains a series of common APIs. Details: CommonService

---
mux:
  - name: greeter                   # Required
    port: 8080                      # Required
    enabled: true                   # Required
    commonService:
      enabled: true                 # Optional, enable common service
    interceptors:
      loggingZap:
        enabled: true               # Optional, enable logging interceptor

2. Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/mux"
	"github.com/rookie-ninja/rk-mux/interceptor"
	"net/http"
)

func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
	entry := rkbootmux.GetMuxEntry("greeter")
	entry.Router.NewRoute().Methods(http.MethodGet).Path("/v1/greeter").HandlerFunc(Greeter)

	// Bootstrap
	boot.Bootstrap(context.TODO())

	boot.WaitForShutdownSig(context.TODO())
}

func Greeter(writer http.ResponseWriter, request *http.Request) {
	rkmuxinter.WriteJson(writer, http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", request.URL.Query().Get("name")),
	})
}

type GreeterResponse struct {
	Message string
}

3. Folder structure

$ tree
.
├── boot.yaml
├── go.mod
├── go.sum
└── main.go

0 directories, 4 files

4. Start main.go

$ go run main.go

2022-02-11T15:43:33.130+0800    INFO    boot/mux_entry.go:643   Bootstrap muxEntry      {"eventId": "1a7f1d5a-13d7-4796-8108-939285d3ec13", "entryName": "greeter", "entryType": "Mux"}
------------------------------------------------------------------------
endTime=2022-02-11T15:43:33.130747+08:00
startTime=2022-02-11T15:43:33.130545+08:00
elapsedNano=202290
timezone=CST
ids={"eventId":"1a7f1d5a-13d7-4796-8108-939285d3ec13"}
app={"appName":"rk","appVersion":"","entryName":"greeter","entryType":"Mux"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"commonServiceEnabled":true,"commonServicePathPrefix":"/rk/v1/","muxPort":8080}
counters={}
pairs={}
timing={}
remoteAddr=localhost
operation=Bootstrap
resCode=OK
eventStatus=Ended
EOE

5. Verify

We send the /rk/v1/healthy request that comes with CommonService.

$ curl -X GET localhost:8080/rk/v1/healthy
{
  "healthy": true
}

Send a request to /v1/greeter.

$ curl "localhost:8080/v1/greeter?name=rk-dev"
{"Message":"Hello rk-dev!"}

EventLog will output to stdout by default.

The log format below is from rk-query , and users can also choose JSON format, which we will introduce later.

------------------------------------------------------------------------
endTime=2022-02-11T15:44:20.00081+08:00
startTime=2022-02-11T15:44:20.000749+08:00
elapsedNano=61165
timezone=CST
ids={"eventId":"c786ff55-78b4-4c44-a268-581aa16def16"}
app={"appName":"rk","appVersion":"","entryName":"greeter","entryType":"Mux"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
counters={}
pairs={}
timing={}
remoteAddr=127.0.0.1:57341
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

Modify log format

We can modify the log format by modifying boot.yaml. Currently supports json and console formats, the default is console.

By modifying the value of eventLoggerEncoding to json, we can output the log in JSON format.

mux:
  - name: greeter                    # Required
    port: 8080                       # Required
    enabled: true                    # Required
    commonService:
      enabled: true                  # Optional, enable common service
    interceptors:
      loggingZap:
        enabled: true                # Optional, enable logging interceptor
        zapLoggerEncoding: "json"    # Override to json format, option: json or console
        eventLoggerEncoding: "json"  # Override to json format, option: json or console
{
  "endTime":"2022-02-11T15:45:27.481+0800",
  "startTime":"2022-02-11T15:45:27.480+0800",
  "elapsedNano":60615,
  "timezone":"CST",
  "ids":{
    "eventId":"f497d8bb-578f-485a-977b-7de7fc5b560e"
  },
  "app":{
    "appName":"rk",
    "appVersion":"",
    "entryName":"greeter",
    "entryType":"Mux"
  },
  "env":{
    "arch":"amd64",
    "az":"*",
    "domain":"*",
    "hostname":"lark.local",
    "localIP":"10.8.0.2",
    "os":"darwin",
    "realm":"*",
    "region":"*"
  },
  "payloads":{
    "apiMethod":"GET",
    "apiPath":"/v1/greeter",
    "apiProtocol":"HTTP/1.1",
    "apiQuery":"name=rk-dev",
    "userAgent":"curl/7.64.1"
  },
  "error":{},
  "counters":{},
  "pairs":{},
  "timing":{},
  "remoteAddr":"127.0.0.1:61646",
  "operation":"/v1/greeter",
  "eventStatus":"Ended",
  "resCode":"200"
}

Modify log path

Output paths can be specified by modifying the value of eventLoggerOutputPaths.

Logs are cut and compressed after 1GB by default.

---
mux:
  - name: greeter                                     # Required
    port: 8080                                        # Required
    enabled: true                                     # Required
    commonService:
      enabled: true                                   # Optional, enable common service
    interceptors:
      loggingZap:
        enabled: true                                 # Optional, enable logging interceptor
        zapLoggerOutputPaths: ["logs/app.log"]        # Override output paths
        eventLoggerOutputPaths: ["logs/event.log"]    # Override output paths
.
├── boot.yaml
├── go.mod
├── go.sum
├── logs
│   └── event.log
└── main.go

Write directly to Loki (remote log storage)

loki is a cloud native log storage, search open source service. Lighter than ElasticSearch , storage cost is very low (can use cloud object storage). The search engine and storage engine of ElasticSearch are very advanced, but the difficulty of operation and maintenance, price, ease of use, and configuration threshold are high, and it is not suitable for simple services.

loki uses the traditional Agent mode to collect logs. rk-boot internally uses a loki-client to transfer logs directly to the Loki service. One aspect of this use is to eliminate the trouble of Agent configuration. Secondly, it is to eliminate the situation of multi-line log. For example, when printing Panic information, we must configure a regular expression in the Agent to tell the Agent to integrate the Panic into a Stream and send it to the Loki service. However, this configuration is very cumbersome.

In the case of a large number of logs, it may affect the speed. Although rk-boot sends logs to Loki asynchronously, the speed will inevitably be affected because of the locks involved.

So, if it is a large log situation, you can use the traditional Agent mode.

1.boot.yaml

Additional definitions for zapLogger, eventLogger, mux.logger.zapLogger & mux.logger.eventLogger.

zapLogger:
  - name: zap-logger
    loki:
      enabled: true
eventLogger:
  - name: event-logger
    loki:
      enabled: true
mux:
  - name: greeter                   # Required
    port: 8080                      # Required
    enabled: true                   # Required
    logger:
      zapLogger: zap-logger         # Optional, reference of zapLogger entry name
      eventLogger: event-logger     # Optional, reference of eventLogger entry name
    interceptors:
      loggingZap:
        enabled: true               # Optional, enable logging interceptor

2. Start Loki locally

To verify, we start Loki locally using Docker.

$ docker run -d --name=loki -p 3100:3100 grafana/loki

3. Start Grafana locally

We use Grafana search to view logs.

$ docker run -p 3000:3000 --name grafana grafana/grafana

Adding Loki data sources in Grafana Grafana is just a web UI tool, in order to see the data report, we tell Grafana where to look for Loki.

Because Grafana runs in Docker, we don't use localhost:9090, but instead, host.docker.internal:9090.

4. Start main.go & send request

$ go run main.go
$ curl "localhost:8080/v1/greeter?name=rk-dev"

5. View logs in Grafana

6. Complete Loki Configuration

---
eventLogger:
  - name: event-logger                 # Required
    loki:
      enabled: true                    # Optional, default: false
      addr: localhost:3100             # Optional, default: localhost:3100
      path: /loki/api/v1/push          # Optional, default: /loki/api/v1/push
      username: ""                     # Optional, default: ""
      password: ""                     # Optional, default: ""
      maxBatchWaitMs: 3000             # Optional, default: 3000
      maxBatchSize: 1000               # Optional, default: 1000
      insecureSkipVerify: false        # Optional, default: false
      labels:                          # Optional, default: empty map
        my_label_key: my_label_value
zapLogger:
  - name: zap-logger                   # Required
    loki:
      enabled: true                    # Optional, default: false
      addr: localhost:3100             # Optional, default: localhost:3100
      path: /loki/api/v1/push          # Optional, default: /loki/api/v1/push
      username: ""                     # Optional, default: ""
      password: ""                     # Optional, default: ""
      maxBatchWaitMs: 3000             # Optional, default: 3000
      maxBatchSize: 1000               # Optional, default: 1000
      insecureSkipVerify: false        # Optional, default: false
      labels:                          # Optional, default: empty map
        my_label_key: my_label_value

concept

After verifying the log interceptor, let's talk about the functions of the log interceptor provided by rk-boot.

We need to understand two concepts in advance.

  • EventLogger
  • ZapLogger

ZapLogger

It is used to record error/detailed logs. Users can obtain the ZapLogger instance of this RPC call and write the log. The ZapLogger instance of each RPC contains the current RequestId.

2022-02-11T15:47:04.571+0800    INFO    boot/mux_entry.go:643   Bootstrap muxEntry      {"eventId": "7b995b92-d77b-4fed-861d-cbfd53738768", "entryName": "greeter", "entryType": "Mux"}

EventLogger

The RK enabler treats each RPC request as an Event and logs it using the Event type in rk-query.

field Details
endTime End Time
startTime Starting time
elapsedNano Event time overhead (Nanoseconds)
timezone Time zone
ids Contains eventId, requestId and traceId. If the original data interceptor is activated, or event.SetRequest() is called by the user, the new RequestId will be used, and the eventId and requestId will be exactly the same. If the call chain interceptor is enabled, the traceId will be logged.
app 包含 appName, appVersion, entryName, entryType。
env Contains arch, az, domain, hostname, localIP, os, realm, region. realm, region, az, domain fields. These fields come from system environment variables (REALM, REGION, AZ, DOMAIN). "*" means the environment variable is empty.
payloads Contains RPC related information.
error contains errors.
counters Operate through event.SetCounter().
pairs Operate through event.AddPair().
timing Operation through event.StartTimer() and event.EndTimer().
remoteAddr RPC remote address.
operation RPC name.
resCode RPC return code.
eventStatus Ended or InProgress
------------------------------------------------------------------------
endTime=2022-02-11T15:44:20.00081+08:00
startTime=2022-02-11T15:44:20.000749+08:00
elapsedNano=61165
timezone=CST
ids={"eventId":"c786ff55-78b4-4c44-a268-581aa16def16"}
app={"appName":"rk","appVersion":"","entryName":"greeter","entryType":"Mux"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
counters={}
pairs={}
timing={}
remoteAddr=127.0.0.1:57341
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

Log middleware options

name describe Types of Defaults
mux.interceptors.loggingZap.enabled Start log interceptor boolean false
mux.interceptors.loggingZap.zapLoggerEncoding Log format: json or console string console
mux.interceptors.loggingZap.zapLoggerOutputPaths log file path []string stdout
mux.interceptors.loggingZap.eventLoggerEncoding Log format: json or console string console
mux.interceptors.loggingZap.eventLoggerOutputPaths log file path []string stdout

Get RPC log instance

Every time an RPC request comes in, the interceptor will inject the RequestId (when the original data interceptor is activated) into the log instance.

In other words, for every RPC request, there will be a new Logger instance. Let's see how to log ZapLogger for an RPC request.

Obtain the log instance of this request through the rkmuxctx.GetLogger(ctx) method.

func Greeter(writer http.ResponseWriter, request *http.Request) {
    rkmuxctx.GetLogger(request, writer).Info("Request received")
	
    rkmuxinter.WriteJson(writer, http.StatusOK, &GreeterResponse{
        Message: fmt.Sprintf("Hello %s!", request.URL.Query().Get("name")),
    })
}

The log prints out!

2022-02-11T15:49:33.513+0800    INFO    mux/main.go:33  Request received

Modify Event

The logging interceptor will create an Event instance for each RPC request.

User can add pairs, counters, errors.

Get the Event instance of this RPC through rkmuxctx.GetEvent(ctx).

func Greeter(writer http.ResponseWriter, request *http.Request) {
    event := rkmuxctx.GetEvent(request)
    event.AddPair("key", "value")

    rkmuxinter.WriteJson(writer, http.StatusOK, &GreeterResponse{
        Message: fmt.Sprintf("Hello %s!", request.URL.Query().Get("name")),
    })
}

Added pairs={"key":"value"} to Event!

------------------------------------------------------------------------
endTime=2022-02-11T15:50:19.286491+08:00
startTime=2022-02-11T15:50:19.286432+08:00
elapsedNano=59508
timezone=CST
ids={"eventId":"382e39dd-c258-4347-b833-c8e391b05777"}
app={"appName":"rk","appVersion":"","entryName":"greeter","entryType":"Mux"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
counters={}
pairs={"key":"value"}
timing={}
remoteAddr=127.0.0.1:63859
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE
{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324130708&siteId=291194637