├── LICENSE ├── README.md ├── examples └── client-example │ ├── .gitignore │ └── main.go ├── logproto ├── logproto.pb.go └── logproto.proto └── promtail ├── common.go ├── jsonclient.go └── protoclient.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aleksander Alekseev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # promtail-client 2 | 3 | Promtail client library. Promtail is an agent for [Loki](https://github.com/grafana/loki) logging system. 4 | 5 | This library supports both JSON and Protobuf APIs. 6 | 7 | Usage example: 8 | 9 | ``` 10 | cd examples/client-example 11 | go build 12 | 13 | # make sure source-name is unique for every application instance 14 | # otherwise promtail will reject logs with error: 15 | # "entry out of order for stream" 16 | ./client-example proto source-name job-name 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/client-example/.gitignore: -------------------------------------------------------------------------------- 1 | client-example 2 | -------------------------------------------------------------------------------- /examples/client-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | "github.com/afiskon/promtail-client/promtail" 9 | ) 10 | 11 | func displayUsage() { 12 | fmt.Fprintf(os.Stderr, "Usage: %s proto|json source-name job-name\n", os.Args[0]) 13 | os.Exit(1) 14 | } 15 | 16 | func displayInvalidName(arg string) { 17 | fmt.Fprintf(os.Stderr, "Invalid %s: allowed characters are a-zA-Z0-9_-\n", arg) 18 | os.Exit(1) 19 | } 20 | 21 | func nameIsValid(name string) bool { 22 | for _, c := range name { 23 | if !((c >= 'a' && c <= 'z') || 24 | (c >= 'A' && c <= 'Z') || 25 | (c >= '0' && c <= '9') || 26 | (c == '-') || (c == '_')) { 27 | return false 28 | } 29 | } 30 | return true 31 | } 32 | 33 | func main() { 34 | if len(os.Args) < 4 { 35 | displayUsage() 36 | } 37 | 38 | format := os.Args[1] 39 | source_name := os.Args[2] 40 | job_name := os.Args[3] 41 | if format != "proto" && format != "json" { 42 | displayUsage() 43 | } 44 | 45 | if !nameIsValid(source_name) { 46 | displayInvalidName("source-name") 47 | } 48 | 49 | if !nameIsValid(job_name) { 50 | displayInvalidName("job-name") 51 | } 52 | 53 | labels := "{source=\""+source_name+"\",job=\""+job_name+"\"}" 54 | conf := promtail.ClientConfig{ 55 | PushURL: "http://localhost:3100/api/prom/push", 56 | Labels: labels, 57 | BatchWait: 5 * time.Second, 58 | BatchEntriesNumber: 10000, 59 | SendLevel: promtail.INFO, 60 | PrintLevel: promtail.ERROR, 61 | } 62 | 63 | var ( 64 | loki promtail.Client 65 | err error 66 | ) 67 | 68 | if format == "proto" { 69 | loki, err = promtail.NewClientProto(conf) 70 | } else { 71 | loki, err = promtail.NewClientJson(conf) 72 | } 73 | 74 | if err != nil { 75 | log.Printf("promtail.NewClient: %s\n", err) 76 | os.Exit(1) 77 | } 78 | 79 | for i := 1; i < 5; i++ { 80 | tstamp := time.Now().String() 81 | loki.Debugf("source = %s time = %s, i = %d\n", source_name, tstamp, i) 82 | loki.Infof("source = %s, time = %s, i = %d\n", source_name, tstamp, i) 83 | loki.Warnf("source = %s, time = %s, i = %d\n", source_name, tstamp, i) 84 | loki.Errorf("source = %s, time = %s, i = %d\n", source_name, tstamp, i) 85 | time.Sleep(1 * time.Second) 86 | } 87 | 88 | loki.Shutdown() 89 | } 90 | -------------------------------------------------------------------------------- /logproto/logproto.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: logproto.proto 3 | 4 | package logproto 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 10 | math "math" 11 | ) 12 | 13 | // Reference imports to suppress errors if they are not otherwise used. 14 | var _ = proto.Marshal 15 | var _ = fmt.Errorf 16 | var _ = math.Inf 17 | 18 | // This is a compile-time assertion to ensure that this generated file 19 | // is compatible with the proto package it is being compiled against. 20 | // A compilation error at this line likely means your copy of the 21 | // proto package needs to be updated. 22 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 23 | 24 | type PushRequest struct { 25 | Streams []*Stream `protobuf:"bytes,1,rep,name=streams,proto3" json:"streams,omitempty"` 26 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 27 | XXX_unrecognized []byte `json:"-"` 28 | XXX_sizecache int32 `json:"-"` 29 | } 30 | 31 | func (m *PushRequest) Reset() { *m = PushRequest{} } 32 | func (m *PushRequest) String() string { return proto.CompactTextString(m) } 33 | func (*PushRequest) ProtoMessage() {} 34 | func (*PushRequest) Descriptor() ([]byte, []int) { 35 | return fileDescriptor_7a8976f235a02f79, []int{0} 36 | } 37 | 38 | func (m *PushRequest) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_PushRequest.Unmarshal(m, b) 40 | } 41 | func (m *PushRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_PushRequest.Marshal(b, m, deterministic) 43 | } 44 | func (m *PushRequest) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_PushRequest.Merge(m, src) 46 | } 47 | func (m *PushRequest) XXX_Size() int { 48 | return xxx_messageInfo_PushRequest.Size(m) 49 | } 50 | func (m *PushRequest) XXX_DiscardUnknown() { 51 | xxx_messageInfo_PushRequest.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_PushRequest proto.InternalMessageInfo 55 | 56 | func (m *PushRequest) GetStreams() []*Stream { 57 | if m != nil { 58 | return m.Streams 59 | } 60 | return nil 61 | } 62 | 63 | type Stream struct { 64 | Labels string `protobuf:"bytes,1,opt,name=labels,proto3" json:"labels,omitempty"` 65 | Entries []*Entry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"` 66 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 67 | XXX_unrecognized []byte `json:"-"` 68 | XXX_sizecache int32 `json:"-"` 69 | } 70 | 71 | func (m *Stream) Reset() { *m = Stream{} } 72 | func (m *Stream) String() string { return proto.CompactTextString(m) } 73 | func (*Stream) ProtoMessage() {} 74 | func (*Stream) Descriptor() ([]byte, []int) { 75 | return fileDescriptor_7a8976f235a02f79, []int{1} 76 | } 77 | 78 | func (m *Stream) XXX_Unmarshal(b []byte) error { 79 | return xxx_messageInfo_Stream.Unmarshal(m, b) 80 | } 81 | func (m *Stream) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 82 | return xxx_messageInfo_Stream.Marshal(b, m, deterministic) 83 | } 84 | func (m *Stream) XXX_Merge(src proto.Message) { 85 | xxx_messageInfo_Stream.Merge(m, src) 86 | } 87 | func (m *Stream) XXX_Size() int { 88 | return xxx_messageInfo_Stream.Size(m) 89 | } 90 | func (m *Stream) XXX_DiscardUnknown() { 91 | xxx_messageInfo_Stream.DiscardUnknown(m) 92 | } 93 | 94 | var xxx_messageInfo_Stream proto.InternalMessageInfo 95 | 96 | func (m *Stream) GetLabels() string { 97 | if m != nil { 98 | return m.Labels 99 | } 100 | return "" 101 | } 102 | 103 | func (m *Stream) GetEntries() []*Entry { 104 | if m != nil { 105 | return m.Entries 106 | } 107 | return nil 108 | } 109 | 110 | type Entry struct { 111 | Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 112 | Line string `protobuf:"bytes,2,opt,name=line,proto3" json:"line,omitempty"` 113 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 114 | XXX_unrecognized []byte `json:"-"` 115 | XXX_sizecache int32 `json:"-"` 116 | } 117 | 118 | func (m *Entry) Reset() { *m = Entry{} } 119 | func (m *Entry) String() string { return proto.CompactTextString(m) } 120 | func (*Entry) ProtoMessage() {} 121 | func (*Entry) Descriptor() ([]byte, []int) { 122 | return fileDescriptor_7a8976f235a02f79, []int{2} 123 | } 124 | 125 | func (m *Entry) XXX_Unmarshal(b []byte) error { 126 | return xxx_messageInfo_Entry.Unmarshal(m, b) 127 | } 128 | func (m *Entry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 129 | return xxx_messageInfo_Entry.Marshal(b, m, deterministic) 130 | } 131 | func (m *Entry) XXX_Merge(src proto.Message) { 132 | xxx_messageInfo_Entry.Merge(m, src) 133 | } 134 | func (m *Entry) XXX_Size() int { 135 | return xxx_messageInfo_Entry.Size(m) 136 | } 137 | func (m *Entry) XXX_DiscardUnknown() { 138 | xxx_messageInfo_Entry.DiscardUnknown(m) 139 | } 140 | 141 | var xxx_messageInfo_Entry proto.InternalMessageInfo 142 | 143 | func (m *Entry) GetTimestamp() *timestamp.Timestamp { 144 | if m != nil { 145 | return m.Timestamp 146 | } 147 | return nil 148 | } 149 | 150 | func (m *Entry) GetLine() string { 151 | if m != nil { 152 | return m.Line 153 | } 154 | return "" 155 | } 156 | 157 | func init() { 158 | proto.RegisterType((*PushRequest)(nil), "logproto.PushRequest") 159 | proto.RegisterType((*Stream)(nil), "logproto.Stream") 160 | proto.RegisterType((*Entry)(nil), "logproto.Entry") 161 | } 162 | 163 | func init() { proto.RegisterFile("logproto.proto", fileDescriptor_7a8976f235a02f79) } 164 | 165 | var fileDescriptor_7a8976f235a02f79 = []byte{ 166 | // 208 bytes of a gzipped FileDescriptorProto 167 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcb, 0xc9, 0x4f, 0x2f, 168 | 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x03, 0x93, 0x42, 0x1c, 0x30, 0xbe, 0x94, 0x7c, 0x7a, 0x7e, 0x7e, 169 | 0x7a, 0x4e, 0xaa, 0x3e, 0x98, 0x97, 0x54, 0x9a, 0xa6, 0x5f, 0x92, 0x99, 0x9b, 0x5a, 0x5c, 0x92, 170 | 0x98, 0x5b, 0x00, 0x51, 0xaa, 0x64, 0xc9, 0xc5, 0x1d, 0x50, 0x5a, 0x9c, 0x11, 0x94, 0x5a, 0x58, 171 | 0x9a, 0x5a, 0x5c, 0x22, 0xa4, 0xc5, 0xc5, 0x5e, 0x5c, 0x52, 0x94, 0x9a, 0x98, 0x5b, 0x2c, 0xc1, 172 | 0xa8, 0xc0, 0xac, 0xc1, 0x6d, 0x24, 0xa0, 0x07, 0x37, 0x3b, 0x18, 0x2c, 0x11, 0x04, 0x53, 0xa0, 173 | 0xe4, 0xcd, 0xc5, 0x06, 0x11, 0x12, 0x12, 0xe3, 0x62, 0xcb, 0x49, 0x4c, 0x4a, 0xcd, 0x01, 0x69, 174 | 0x62, 0xd4, 0xe0, 0x0c, 0x82, 0xf2, 0x84, 0x34, 0xb9, 0xd8, 0x53, 0xf3, 0x4a, 0x8a, 0x32, 0x53, 175 | 0x8b, 0x25, 0x98, 0xc0, 0xa6, 0xf1, 0x23, 0x4c, 0x73, 0xcd, 0x2b, 0x29, 0xaa, 0x0c, 0x82, 0xc9, 176 | 0x2b, 0x85, 0x72, 0xb1, 0x82, 0x45, 0x84, 0x2c, 0xb8, 0x38, 0xe1, 0x6e, 0x04, 0x1b, 0xc7, 0x6d, 177 | 0x24, 0xa5, 0x07, 0xf1, 0x85, 0x1e, 0xcc, 0x17, 0x7a, 0x21, 0x30, 0x15, 0x41, 0x08, 0xc5, 0x42, 178 | 0x42, 0x5c, 0x2c, 0x39, 0x99, 0x79, 0xa9, 0x12, 0x4c, 0x60, 0x37, 0x80, 0xd9, 0x49, 0x6c, 0x60, 179 | 0x2d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x63, 0xb9, 0x3c, 0x7e, 0x22, 0x01, 0x00, 0x00, 180 | } 181 | -------------------------------------------------------------------------------- /logproto/logproto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package logproto; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message PushRequest { 8 | repeated Stream streams = 1; 9 | } 10 | 11 | message Stream { 12 | string labels = 1; 13 | repeated Entry entries = 2; 14 | } 15 | 16 | message Entry { 17 | google.protobuf.Timestamp timestamp = 1; 18 | string line = 2; 19 | } 20 | -------------------------------------------------------------------------------- /promtail/common.go: -------------------------------------------------------------------------------- 1 | package promtail 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | const LOG_ENTRIES_CHAN_SIZE = 5000 11 | 12 | type LogLevel int 13 | 14 | const ( 15 | DEBUG LogLevel = iota 16 | INFO LogLevel = iota 17 | WARN LogLevel = iota 18 | ERROR LogLevel = iota 19 | // Maximum level, disables sending or printing 20 | DISABLE LogLevel = iota 21 | ) 22 | 23 | type ClientConfig struct { 24 | // E.g. http://localhost:3100/api/prom/push 25 | PushURL string 26 | // E.g. "{job=\"somejob\"}" 27 | Labels string 28 | BatchWait time.Duration 29 | BatchEntriesNumber int 30 | // Logs are sent to Promtail if the entry level is >= SendLevel 31 | SendLevel LogLevel 32 | // Logs are printed to stdout if the entry level is >= PrintLevel 33 | PrintLevel LogLevel 34 | } 35 | 36 | type Client interface { 37 | Debugf(format string, args ...interface{}) 38 | Infof(format string, args ...interface{}) 39 | Warnf(format string, args ...interface{}) 40 | Errorf(format string, args ...interface{}) 41 | Shutdown() 42 | } 43 | 44 | // http.Client wrapper for adding new methods, particularly sendJsonReq 45 | type httpClient struct { 46 | parent http.Client 47 | } 48 | 49 | // A bit more convenient method for sending requests to the HTTP server 50 | func (client *httpClient) sendJsonReq(method, url string, ctype string, reqBody []byte) (resp *http.Response, resBody []byte, err error) { 51 | req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | 56 | req.Header.Set("Content-Type", ctype) 57 | 58 | resp, err = client.parent.Do(req) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | defer resp.Body.Close() 63 | 64 | resBody, err = ioutil.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | return resp, resBody, nil 70 | } -------------------------------------------------------------------------------- /promtail/jsonclient.go: -------------------------------------------------------------------------------- 1 | package promtail 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type jsonLogEntry struct { 12 | Ts time.Time `json:"ts"` 13 | Line string `json:"line"` 14 | level LogLevel // not used in JSON 15 | } 16 | 17 | type promtailStream struct { 18 | Labels string `json:"labels"` 19 | Entries []*jsonLogEntry `json:"entries"` 20 | } 21 | 22 | type promtailMsg struct { 23 | Streams []promtailStream `json:"streams"` 24 | } 25 | 26 | type clientJson struct { 27 | config *ClientConfig 28 | quit chan struct{} 29 | entries chan *jsonLogEntry 30 | waitGroup sync.WaitGroup 31 | client httpClient 32 | } 33 | 34 | func NewClientJson(conf ClientConfig) (Client, error) { 35 | client := clientJson{ 36 | config: &conf, 37 | quit: make(chan struct{}), 38 | entries: make(chan *jsonLogEntry, LOG_ENTRIES_CHAN_SIZE), 39 | client: httpClient{}, 40 | } 41 | 42 | client.waitGroup.Add(1) 43 | go client.run() 44 | 45 | return &client, nil 46 | } 47 | 48 | func (c *clientJson) Debugf(format string, args ...interface{}) { 49 | c.log(format, DEBUG, "Debug: ", args...) 50 | } 51 | 52 | func (c *clientJson) Infof(format string, args ...interface{}) { 53 | c.log(format, INFO, "Info: ", args...) 54 | } 55 | 56 | func (c *clientJson) Warnf(format string, args ...interface{}) { 57 | c.log(format, WARN, "Warn: ", args...) 58 | } 59 | 60 | func (c *clientJson) Errorf(format string, args ...interface{}) { 61 | c.log(format, ERROR, "Error: ", args...) 62 | } 63 | 64 | func (c *clientJson) log(format string, level LogLevel, prefix string, args ...interface{}) { 65 | if (level >= c.config.SendLevel) || (level >= c.config.PrintLevel) { 66 | c.entries <- &jsonLogEntry{ 67 | Ts: time.Now(), 68 | Line: fmt.Sprintf(prefix+format, args...), 69 | level: level, 70 | } 71 | } 72 | } 73 | 74 | func (c *clientJson) Shutdown() { 75 | close(c.quit) 76 | c.waitGroup.Wait() 77 | } 78 | 79 | func (c *clientJson) run() { 80 | var batch []*jsonLogEntry 81 | batchSize := 0 82 | maxWait := time.NewTimer(c.config.BatchWait) 83 | 84 | defer func() { 85 | if batchSize > 0 { 86 | c.send(batch) 87 | } 88 | 89 | c.waitGroup.Done() 90 | }() 91 | 92 | for { 93 | select { 94 | case <-c.quit: 95 | return 96 | case entry := <-c.entries: 97 | if entry.level >= c.config.PrintLevel { 98 | log.Print(entry.Line) 99 | } 100 | 101 | if entry.level >= c.config.SendLevel { 102 | batch = append(batch, entry) 103 | batchSize++ 104 | if batchSize >= c.config.BatchEntriesNumber { 105 | c.send(batch) 106 | batch = []*jsonLogEntry{} 107 | batchSize = 0 108 | maxWait.Reset(c.config.BatchWait) 109 | } 110 | } 111 | case <-maxWait.C: 112 | if batchSize > 0 { 113 | c.send(batch) 114 | batch = []*jsonLogEntry{} 115 | batchSize = 0 116 | } 117 | maxWait.Reset(c.config.BatchWait) 118 | } 119 | } 120 | } 121 | 122 | func (c *clientJson) send(entries []*jsonLogEntry) { 123 | var streams []promtailStream 124 | streams = append(streams, promtailStream{ 125 | Labels: c.config.Labels, 126 | Entries: entries, 127 | }) 128 | 129 | msg := promtailMsg{Streams: streams} 130 | jsonMsg, err := json.Marshal(msg) 131 | if err != nil { 132 | log.Printf("promtail.ClientJson: unable to marshal a JSON document: %s\n", err) 133 | return 134 | } 135 | 136 | resp, body, err := c.client.sendJsonReq("POST", c.config.PushURL, "application/json", jsonMsg) 137 | if err != nil { 138 | log.Printf("promtail.ClientJson: unable to send an HTTP request: %s\n", err) 139 | return 140 | } 141 | 142 | if resp.StatusCode != 204 { 143 | log.Printf("promtail.ClientJson: Unexpected HTTP status code: %d, message: %s\n", resp.StatusCode, body) 144 | return 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /promtail/protoclient.go: -------------------------------------------------------------------------------- 1 | package promtail 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang/protobuf/proto" 6 | "github.com/golang/protobuf/ptypes/timestamp" 7 | "github.com/golang/snappy" 8 | "github.com/afiskon/promtail-client/logproto" 9 | "log" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type protoLogEntry struct { 15 | entry *logproto.Entry 16 | level LogLevel 17 | } 18 | 19 | type clientProto struct { 20 | config *ClientConfig 21 | quit chan struct{} 22 | entries chan protoLogEntry 23 | waitGroup sync.WaitGroup 24 | client httpClient 25 | } 26 | 27 | func NewClientProto(conf ClientConfig) (Client, error) { 28 | client := clientProto{ 29 | config: &conf, 30 | quit: make(chan struct{}), 31 | entries: make(chan protoLogEntry, LOG_ENTRIES_CHAN_SIZE), 32 | client: httpClient{}, 33 | } 34 | 35 | client.waitGroup.Add(1) 36 | go client.run() 37 | 38 | return &client, nil 39 | } 40 | 41 | func (c *clientProto) Debugf(format string, args ...interface{}) { 42 | c.log(format, DEBUG, "Debug: ", args...) 43 | } 44 | 45 | func (c *clientProto) Infof(format string, args ...interface{}) { 46 | c.log(format, INFO, "Info: ", args...) 47 | } 48 | 49 | func (c *clientProto) Warnf(format string, args ...interface{}) { 50 | c.log(format, WARN, "Warn: ", args...) 51 | } 52 | 53 | func (c *clientProto) Errorf(format string, args ...interface{}) { 54 | c.log(format, ERROR, "Error: ", args...) 55 | } 56 | 57 | func (c *clientProto) log(format string, level LogLevel, prefix string, args ...interface{}) { 58 | if (level >= c.config.SendLevel) || (level >= c.config.PrintLevel) { 59 | now := time.Now().UnixNano() 60 | c.entries <- protoLogEntry{ 61 | entry: &logproto.Entry{ 62 | Timestamp: ×tamp.Timestamp{ 63 | Seconds: now / int64(time.Second), 64 | Nanos: int32(now % int64(time.Second)), 65 | }, 66 | Line: fmt.Sprintf(prefix+format, args...), 67 | }, 68 | level: level, 69 | } 70 | } 71 | } 72 | 73 | func (c *clientProto) Shutdown() { 74 | close(c.quit) 75 | c.waitGroup.Wait() 76 | } 77 | 78 | func (c *clientProto) run() { 79 | var batch []*logproto.Entry 80 | batchSize := 0 81 | maxWait := time.NewTimer(c.config.BatchWait) 82 | 83 | defer func() { 84 | if batchSize > 0 { 85 | c.send(batch) 86 | } 87 | 88 | c.waitGroup.Done() 89 | }() 90 | 91 | for { 92 | select { 93 | case <-c.quit: 94 | return 95 | case entry := <-c.entries: 96 | if entry.level >= c.config.PrintLevel { 97 | log.Print(entry.entry.Line) 98 | } 99 | 100 | if entry.level >= c.config.SendLevel { 101 | batch = append(batch, entry.entry) 102 | batchSize++ 103 | if batchSize >= c.config.BatchEntriesNumber { 104 | c.send(batch) 105 | batch = []*logproto.Entry{} 106 | batchSize = 0 107 | maxWait.Reset(c.config.BatchWait) 108 | } 109 | } 110 | case <-maxWait.C: 111 | if batchSize > 0 { 112 | c.send(batch) 113 | batch = []*logproto.Entry{} 114 | batchSize = 0 115 | } 116 | maxWait.Reset(c.config.BatchWait) 117 | } 118 | } 119 | } 120 | 121 | func (c *clientProto) send(entries []*logproto.Entry) { 122 | var streams []*logproto.Stream 123 | streams = append(streams, &logproto.Stream{ 124 | Labels: c.config.Labels, 125 | Entries: entries, 126 | }) 127 | 128 | req := logproto.PushRequest{ 129 | Streams: streams, 130 | } 131 | 132 | buf, err := proto.Marshal(&req) 133 | if err != nil { 134 | log.Printf("promtail.ClientProto: unable to marshal: %s\n", err) 135 | return 136 | } 137 | 138 | buf = snappy.Encode(nil, buf) 139 | 140 | resp, body, err := c.client.sendJsonReq("POST", c.config.PushURL, "application/x-protobuf", buf) 141 | if err != nil { 142 | log.Printf("promtail.ClientProto: unable to send an HTTP request: %s\n", err) 143 | return 144 | } 145 | 146 | if resp.StatusCode != 204 { 147 | log.Printf("promtail.ClientProto: Unexpected HTTP status code: %d, message: %s\n", resp.StatusCode, body) 148 | return 149 | } 150 | } 151 | --------------------------------------------------------------------------------