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 }