github.com/binkynet/BinkyNet@v1.12.1-0.20240421190447-da4e34c20be0/loki/logger.go (about)

     1  // Copyright 2022 Ewout Prangsma
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // Author Ewout Prangsma
    16  //
    17  
    18  package loki
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"log"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/rs/zerolog"
    32  )
    33  
    34  // LokiLogger send log messages towards Loki.
    35  type LokiLogger struct {
    36  	config     *clientConfig
    37  	quit       chan struct{}
    38  	entries    chan logEntry
    39  	waitGroup  sync.WaitGroup
    40  	client     httpClient
    41  	timeOffset int64
    42  }
    43  
    44  type PushRequest struct {
    45  	Streams []StreamAdapter `json:"streams"`
    46  }
    47  
    48  type StreamAdapter struct {
    49  	Stream map[string]string `json:"stream"`
    50  	Values [][]string        `json:"values"`
    51  }
    52  
    53  type logEntry struct {
    54  	Timestamp time.Time
    55  	Line      string
    56  	Level     zerolog.Level
    57  }
    58  
    59  func NewLokiLogger(rootUrl, job string, timeOffset int64) (*LokiLogger, error) {
    60  	conf := &clientConfig{
    61  		PushURL:            strings.TrimSuffix(rootUrl, "/") + "/loki/api/v1/push",
    62  		BatchWait:          time.Second * 2,
    63  		BatchEntriesNumber: 1024,
    64  		Labels: map[string]string{
    65  			"job": job,
    66  		},
    67  	}
    68  	client := &LokiLogger{
    69  		config:     conf,
    70  		quit:       make(chan struct{}),
    71  		entries:    make(chan logEntry, LOG_ENTRIES_CHAN_SIZE),
    72  		client:     httpClient{},
    73  		timeOffset: timeOffset,
    74  	}
    75  
    76  	client.waitGroup.Add(1)
    77  	go client.run()
    78  
    79  	return client, nil
    80  }
    81  
    82  var _ zerolog.LevelWriter = &LokiLogger{}
    83  
    84  // Write a std message
    85  func (l *LokiLogger) Write(p []byte) (int, error) {
    86  	return l.WriteLevel(zerolog.InfoLevel, p)
    87  }
    88  
    89  // Write a message with given level
    90  func (l *LokiLogger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
    91  	now := time.Now()
    92  	l.entries <- logEntry{
    93  		Timestamp: now,
    94  		Line:      string(p),
    95  		Level:     level,
    96  	}
    97  	return len(p), nil
    98  }
    99  
   100  func (c *LokiLogger) Shutdown() {
   101  	close(c.quit)
   102  	c.waitGroup.Wait()
   103  }
   104  
   105  // Set the timeoffset in seconds
   106  func (c *LokiLogger) SetTimeoffset(timeOffset int64) {
   107  	c.timeOffset = timeOffset
   108  }
   109  
   110  func (c *LokiLogger) run() {
   111  	var batch [][]string
   112  	batchSize := 0
   113  	maxWait := time.NewTimer(c.config.BatchWait)
   114  
   115  	defer func() {
   116  		if batchSize > 0 {
   117  			c.send(batch)
   118  		}
   119  
   120  		c.waitGroup.Done()
   121  	}()
   122  
   123  	for {
   124  		select {
   125  		case <-c.quit:
   126  			return
   127  		case entry := <-c.entries:
   128  			ts := entry.Timestamp
   129  			if timeOffset := c.timeOffset; timeOffset != 0 {
   130  				ts = ts.Add(time.Second * time.Duration(timeOffset))
   131  			}
   132  			batch = append(batch, []string{
   133  				strconv.FormatInt(ts.UnixNano(), 10),
   134  				formatEntry(entry.Level, entry.Line),
   135  			})
   136  			batchSize++
   137  			if batchSize >= c.config.BatchEntriesNumber {
   138  				c.send(batch)
   139  				batch = nil
   140  				batchSize = 0
   141  				maxWait.Reset(c.config.BatchWait)
   142  			}
   143  		case <-maxWait.C:
   144  			if batchSize > 0 {
   145  				c.send(batch)
   146  				batch = nil
   147  				batchSize = 0
   148  			}
   149  			maxWait.Reset(c.config.BatchWait)
   150  		}
   151  	}
   152  }
   153  
   154  func (c *LokiLogger) send(entries [][]string) {
   155  	req := PushRequest{
   156  		Streams: []StreamAdapter{
   157  			{
   158  				Stream: c.config.Labels,
   159  				Values: entries,
   160  			},
   161  		},
   162  	}
   163  
   164  	buf, err := json.Marshal(req)
   165  	if err != nil {
   166  		log.Printf("promtail.ClientProto: unable to marshal: %s\n", err)
   167  		return
   168  	}
   169  
   170  	resp, body, err := c.client.sendJsonReq("POST", c.config.PushURL, "application/json", buf)
   171  	if err != nil {
   172  		log.Printf("promtail.ClientProto: unable to send an HTTP request: %s\n", err)
   173  		return
   174  	}
   175  
   176  	if resp.StatusCode != 204 {
   177  		log.Printf("promtail.ClientProto: Unexpected HTTP status code: %d, message: %s\n", resp.StatusCode, body)
   178  		return
   179  	}
   180  }
   181  
   182  func formatEntry(level zerolog.Level, line string) string {
   183  	var evt map[string]interface{}
   184  	d := json.NewDecoder(strings.NewReader(line))
   185  	d.UseNumber()
   186  	if err := d.Decode(&evt); err != nil {
   187  		log.Printf("LokiLogger: Failed to parse log entry '%s': %s\n", line, err)
   188  		return line
   189  	}
   190  	var buf bytes.Buffer
   191  	keys := make([]string, 0, len(evt))
   192  	for k := range evt {
   193  		switch k {
   194  		case zerolog.MessageFieldName, zerolog.TimestampFieldName, zerolog.LevelFieldName:
   195  			// Skip
   196  		default:
   197  			keys = append(keys, k)
   198  		}
   199  	}
   200  	sort.Strings(keys)
   201  	// Level
   202  	buf.WriteString(level.String())
   203  	buf.WriteByte(' ')
   204  	// Message
   205  	if v, ok := evt[zerolog.MessageFieldName]; ok {
   206  		buf.WriteString(fmt.Sprintf("%s", v))
   207  	}
   208  	// Other key=value pairs
   209  	for _, k := range keys {
   210  		buf.WriteByte(' ')
   211  		buf.WriteString(k)
   212  		buf.WriteByte('=')
   213  		v := evt[k]
   214  		switch tv := v.(type) {
   215  		case string:
   216  			if needsQuote(tv) {
   217  				buf.WriteString(strconv.Quote(tv))
   218  			} else {
   219  				buf.WriteString(tv)
   220  			}
   221  		default:
   222  			b, _ := json.Marshal(v)
   223  			buf.Write(b)
   224  		}
   225  	}
   226  	return buf.String()
   227  }
   228  
   229  // needsQuote returns true when the string s should be quoted in output.
   230  func needsQuote(s string) bool {
   231  	for i := range s {
   232  		if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
   233  			return true
   234  		}
   235  	}
   236  	return false
   237  }