ちゃんるいすのブログ

オタクエンジニアの雑記

Nomad に載った API サーバーのログ監視を考える


これの続き
blog.luispc.com

ログ監視、ここではアプリケーションのエラーログについて

Slack に通知をする

これを使う。
github.com

fluentd.conf

<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<filter *.**>
  @type parser
  <parse>
    @type json
    json_parser oj
    time_format %Y-%m-%dT%H:%M:%S
  </parse>
  key_name log
  replace_invalid_sequence true
  emit_invalid_record_to_error true
  reserve_data true
</filter>

<filter *.**>
  @type grep
  <exclude>
    key caller
    pattern /^$/
  </exclude>
</filter>

<match **.*>
  @type copy
  <store>
    @type elasticsearch
    host elasticsearch.luis.local
    port 9200
    logstash_format true
    logstash_prefix fluentd
    logstash_dateformat %Y%m%d
    include_tag_key true
    type_name fluentd
    tag_key @log_name
    flush_interval 1s
    log_es_400_reason true
    <buffer>
      @type memory
      flush_thread_count 4
    </buffer>
  </store>
  <store>
    @type slack
    webhook_url 
    channel easyuploader
    username "easyuploader api"
    color danger
    icon_url 
    flush_interval 1s
    title_keys func
    title "Error in %s"
    message_keys msg,func,error,client,ua
    message "*%s in %s*\n*err:* %s\n*ip:* %s\n*ua:* %s"
    <buffer>
      @type memory
      flush_thread_count 4
    </buffer>
  </store>
</match>

ネストされた JSON を展開する

{
  "_index": "fluentd-20200106",
  "_type": "_doc",
  "_id": "XsbPem8B4S2PoHfN9N2k",
  "_version": 1,
  "_score": null,
  "_source": {
    "container_id": "6b35a88f599432b461bfd7f1cf54a167f60840deb5dfb409a066739cb891de18",
    "container_name": "/prd-31491e51-65c3-2893-afc0-4bc4f8219a5b",
    "source": "stderr",
    "log": "{\"level\":\"error\",\"ts\":1578312131.531616,\"caller\":\"lib/logger.go:22\",\"msg\":\"GetUploadInfo\",\"func\":\"Delete\",\"error\":\"record not found\",\"client\":\"\",\"ua\":\"Mozilla/5.0 (iPad; CPU OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.0 Mobile/14G60 Safari/602.1\",\"stacktrace\":\"github.com/rluisr/easyuploader_api/lib.logger\\n\\t/go/src/easyuploader_api/lib/logger.go:22\\ngithub.com/rluisr/easyuploader_api/lib.ErrorWithResponseInternalServer\\n\\t/go/src/easyuploader_api/lib/logger.go:30\\ngithub.com/rluisr/easyuploader_api/resource.Delete\\n\\t/go/src/easyuploader_api/resource/delete.go:22\\nmain.main.func9\\n\\t/go/src/easyuploader_api/main.go:73\\ngithub.com/gin-gonic/gin.(*Context).Next\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\\ngithub.com/gin-gonic/gin.RecoveryWithWriter.func1\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/recovery.go:83\\ngithub.com/gin-gonic/gin.(*Context).Next\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\\ngo.elastic.co/apm/module/apmgin.(*middleware).handle\\n\\t/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.5.0/middleware.go:132\\ngithub.com/gin-gonic/gin.(*Context).Next\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:389\\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\\n\\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:351\\nnet/http.serverHandler.ServeHTTP\\n\\t/usr/local/go/src/net/http/server.go:2802\\nnet/http.(*conn).serve\\n\\t/usr/local/go/src/net/http/server.go:1890\"}",
    "level": "error",
    "ts": 1578312131.531616,
    "caller": "lib/logger.go:22",
    "msg": "GetUploadInfo",
    "func": "Delete",
    "error": "record not found",
    "client": ",
    "ua": "Mozilla/5.0 (iPad; CPU OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.0 Mobile/14G60 Safari/602.1",
    "stacktrace": "github.com/rluisr/easyuploader_api/lib.logger\n\t/go/src/easyuploader_api/lib/logger.go:22\ngithub.com/rluisr/easyuploader_api/lib.ErrorWithResponseInternalServer\n\t/go/src/easyuploader_api/lib/logger.go:30\ngithub.com/rluisr/easyuploader_api/resource.Delete\n\t/go/src/easyuploader_api/resource/delete.go:22\nmain.main.func9\n\t/go/src/easyuploader_api/main.go:73\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\ngithub.com/gin-gonic/gin.RecoveryWithWriter.func1\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/recovery.go:83\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\ngo.elastic.co/apm/module/apmgin.(*middleware).handle\n\t/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.5.0/middleware.go:132\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:389\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:351\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2802\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1890",
    "@timestamp": "2020-01-06T12:23:11.429817368+00:00",
    "@log_name": "eu-prd-api"
  },
  "fields": {
    "@timestamp": [
      "2020-01-06T12:23:11.429Z"
    ]
  },
  "sort": [
    1578313391429
  ]
}

log の中身に全部入ってるが、それを ↓ によってパースしている。
その結果、msg とか func が展開された。

<filter *.**>
  @type parser
  <parse>
    @type json
    json_parser oj
    time_format %Y-%m-%dT%H:%M:%S
  </parse>
  key_name log
  replace_invalid_sequence true
  emit_invalid_record_to_error true
  reserve_data true
</filter>

が、パラメータとかはよく分かってない。

アプリのログ以外は無視したい

<filter *.**>
  @type grep
  <exclude>
    key caller
    pattern /^$/
  </exclude>
</filter>

caller がないときは無視する。

Elasticsearch に送りながら Slack に送る

<match **.*>
  @type copy
  <store>
    @type elasticsearch
    host elasticsearch.luis.local
    port 9200
    logstash_format true
    logstash_prefix fluentd
    logstash_dateformat %Y%m%d
    include_tag_key true
    type_name fluentd
    tag_key @log_name
    flush_interval 1s
    log_es_400_reason true
    <buffer>
      @type memory
      flush_thread_count 4
    </buffer>
  </store>
  <store>
    @type slack
    webhook_url 
    channel easyuploader
    username "easyuploader api"
    color danger
    icon_url
    flush_interval 1s
    title_keys func
    title "Error in %s"
    message_keys msg,func,error,client,ua
    message "*%s in %s*\n*err:* %s\n*ip:* %s\n*ua:* %s"
    <buffer>
      @type memory
      flush_thread_count 4
    </buffer>
  </store>
</match>

確認

f:id:rarirureluis:20200106221153p:plain
Slack 上では必要最低限な情報に留めて、Stack trace は Kibana 上で確認できるように ts を載せる。
出来れば footer, author で Kibana の該当レコードへのリンクとか貼りたい。

Tips

これを実現するために改修したこと

ロガー

func logger(funcName, msg, clientIP, ua string, err error) {
    slogger.Errorw(msg,
	"func", funcName,
	"error", err,
	"client", clientIP,
	"ua", ua)
}

func ErrorWithResponseInternalServer(c *gin.Context, funcName, msg, resMsg string, err error) {
    logger(funcName, msg, c.ClientIP(), c.Request.UserAgent(), err)
    c.JSON(http.StatusInternalServerError, resMsg)
}

func ErrorWithResponseBadRequest(c *gin.Context, funcName, msg, resMsg string, err error) {
    logger(funcName, msg, c.ClientIP(), c.Request.UserAgent(), err)
    c.JSON(http.StatusBadRequest, resMsg)
}

Gin を Graceful shutdown に対応させる

func main() {
    router := gin.New()

    srv := &http.Server{
	Addr:    addr,
	Handler: router,
}

    go func() {
	if err := srv.ListenAndServe(); err != nil {
	    log.Printf("listen: %s\n", err)
	}
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    log.Println("Shutdown Server ...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
	log.Fatal("Server Shutdown:", err)
    }
    log.Println("Server exiting")
}

Gin が Proxy 配下にいるときにクライアント IP を正しく出力する

router.ForwardedByClientIP = true

ちなみに中身はリクエストヘッダーを見てる

// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string {
    if c.engine.ForwardedByClientIP {
	clientIP := c.requestHeader("X-Forwarded-For")
	clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
	if clientIP == "" {
	    clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
	}
	if clientIP != "" {
	    return clientIP
	}
    }

    if c.engine.AppEngine {
	if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
	    return addr
	}
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
	return ip
    }

    return ""
}

Traefik で X-Forwarded-* を流さずにそのまま流す

今回の場合は Traefik が Nginx の下にいる。

[entryPoints.http.forwardedHeaders]
      insecure = true

insecure 以外にも、trustedIPs でも指定できるけど両方指定すると動かないので注意。
注意深い人は trustedIPs の方が良いと思う。

docs.traefik.io


GoLand から直接 はてなブログにコピペするとインデントがズレる