bilibili-backup/app/service/main/dapper-query/pkg/opslog/opslog.go
2019-04-22 02:59:20 +00:00

222 lines
5.5 KiB
Go

// Package opslog provide ops-log api
package opslog
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
)
const (
_kbnVersion = "5.4.3"
_indexPrefix = "billions-"
_mqueryContentType = "application/x-ndjson"
_ajsSessioID = "_AJSESSIONID"
)
// Err errors
var (
ErrOverRange = errors.New("search time over range")
)
type response struct {
Hits struct {
Hits []struct {
Source map[string]interface{} `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
// Record represent log record
type Record struct {
Time time.Time `json:"timestamp"`
Fields map[string]interface{} `json:"fields"`
Level string `json:"level"`
Message string `json:"message"`
}
// Client query log from ops-log
type Client interface {
Query(ctx context.Context, familys []string, traceID uint64, sessionID string, start, end int64, options ...Option) ([]*Record, error)
}
type option struct {
traceField string
size int
level string
}
var _defaultOpt = option{
traceField: "traceid",
size: 100,
}
// Option for query
type Option func(opt *option)
// SetTraceField default "traceid"
func SetTraceField(traceField string) Option {
return func(opt *option) {
opt.traceField = traceField
}
}
// SetSize default 100
func SetSize(size int) Option {
return func(opt *option) {
opt.size = size
}
}
// SetLevel return all if level is empty
func SetLevel(level string) Option {
return func(opt *option) {
opt.level = level
}
}
// New ops-log client
func New(searchAPI string, httpClient *http.Client) Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &client{
searchAPI: searchAPI,
httpclient: httpClient,
}
}
type client struct {
searchAPI string
httpclient *http.Client
}
func (c *client) Query(ctx context.Context, familys []string, traceID uint64, sessionID string, start, end int64, options ...Option) ([]*Record, error) {
if start <= 0 || end <= 0 {
return nil, ErrOverRange
}
if len(familys) == 0 {
return make([]*Record, 0), nil
}
opt := _defaultOpt
for _, fn := range options {
fn(&opt)
}
req, err := c.newReq(familys, traceID, sessionID, start, end, &opt)
if err != nil {
return nil, err
}
resp, err := c.httpclient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to %s fail", c.searchAPI)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
buf := make([]byte, 1024)
n, _ := resp.Body.Read(buf)
return nil, errors.Errorf("ops-log response error: status_code: %d, body: %s", resp.StatusCode, buf[:n])
}
return decodeRecord(resp.Body)
}
func (c *client) newReq(familys []string, traceID uint64, sessionID string, start, end int64, opt *option) (*http.Request, error) {
prefixTraceID := strconv.FormatUint(traceID, 16)
leagcyTraceID := strconv.FormatUint(traceID, 10)
startMillis := start * int64((time.Second / time.Millisecond))
endMillis := end * int64((time.Second / time.Millisecond))
body := &bytes.Buffer{}
enc := json.NewEncoder(body)
header := map[string]interface{}{"index": formatIndices(familys), "ignore_unavailable": true}
if err := enc.Encode(header); err != nil {
return nil, err
}
shoulds := []map[string]interface{}{
{"prefix": map[string]interface{}{opt.traceField: prefixTraceID}},
{"match": map[string]interface{}{opt.traceField: leagcyTraceID}},
}
traceQuery := map[string]interface{}{"bool": map[string]interface{}{"should": shoulds}}
rangeQuery := map[string]interface{}{
"range": map[string]interface{}{
"@timestamp": map[string]interface{}{"gte": startMillis, "lte": endMillis, "format": "epoch_millis"},
},
}
musts := []map[string]interface{}{traceQuery, rangeQuery}
if opt.level != "" {
musts = append(musts, map[string]interface{}{"match": map[string]interface{}{"level": opt.level}})
}
query := map[string]interface{}{
"sort": map[string]interface{}{
"@timestamp": map[string]interface{}{
"order": "desc",
"unmapped_type": "boolean",
},
},
"query": map[string]interface{}{
"bool": map[string]interface{}{"must": musts},
},
"version": true,
"size": opt.size,
}
if err := enc.Encode(query); err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, c.searchAPI, body)
if err != nil {
return nil, err
}
session := &http.Cookie{Name: _ajsSessioID, Value: sessionID}
req.AddCookie(session)
req.Header.Set("Content-Type", _mqueryContentType)
req.Header.Set("kbn-version", _kbnVersion)
return req, nil
}
func decodeRecord(src io.Reader) ([]*Record, error) {
var resp struct {
Responses []response `json:"responses"`
}
if err := json.NewDecoder(src).Decode(&resp); err != nil {
return nil, errors.Wrap(err, "decode response error")
}
if len(resp.Responses) == 0 {
return nil, nil
}
records := make([]*Record, 0, len(resp.Responses[0].Hits.Hits))
for _, hit := range resp.Responses[0].Hits.Hits {
record := &Record{
Fields: make(map[string]interface{}),
}
for k, v := range hit.Source {
switch k {
case "@timestamp":
s, _ := v.(string)
record.Time, _ = time.Parse(time.RFC3339Nano, s)
case "log":
s, _ := v.(string)
record.Message = s
case "level":
s, _ := v.(string)
record.Level = s
default:
record.Fields[k] = v
}
}
records = append(records, record)
}
return records, nil
}
func formatIndices(familys []string) []string {
indices := make([]string, len(familys))
for i := range familys {
indices[i] = _indexPrefix + familys[i] + "*"
}
return indices
}