github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/targets/docker/target.go (about) 1 package docker 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 docker_types "github.com/docker/docker/api/types" 14 "github.com/docker/docker/client" 15 "github.com/docker/docker/pkg/stdcopy" 16 "github.com/go-kit/log" 17 "github.com/go-kit/log/level" 18 "github.com/prometheus/common/model" 19 "github.com/prometheus/prometheus/model/labels" 20 "github.com/prometheus/prometheus/model/relabel" 21 "go.uber.org/atomic" 22 23 "github.com/grafana/loki/clients/pkg/promtail/api" 24 "github.com/grafana/loki/clients/pkg/promtail/positions" 25 "github.com/grafana/loki/clients/pkg/promtail/targets/target" 26 27 "github.com/grafana/loki/pkg/logproto" 28 ) 29 30 type Target struct { 31 logger log.Logger 32 handler api.EntryHandler 33 since int64 34 positions positions.Positions 35 containerName string 36 labels model.LabelSet 37 relabelConfig []*relabel.Config 38 metrics *Metrics 39 40 cancel context.CancelFunc 41 client client.APIClient 42 wg sync.WaitGroup 43 running *atomic.Bool 44 err error 45 } 46 47 func NewTarget( 48 metrics *Metrics, 49 logger log.Logger, 50 handler api.EntryHandler, 51 position positions.Positions, 52 containerName string, 53 labels model.LabelSet, 54 relabelConfig []*relabel.Config, 55 client client.APIClient, 56 ) (*Target, error) { 57 58 pos, err := position.Get(positions.CursorKey(containerName)) 59 if err != nil { 60 return nil, err 61 } 62 var since int64 63 if pos != 0 { 64 since = pos 65 } 66 67 t := &Target{ 68 logger: logger, 69 handler: handler, 70 since: since, 71 positions: position, 72 containerName: containerName, 73 labels: labels, 74 relabelConfig: relabelConfig, 75 metrics: metrics, 76 77 client: client, 78 running: atomic.NewBool(false), 79 } 80 t.startIfNotRunning() 81 return t, nil 82 } 83 84 func (t *Target) processLoop(ctx context.Context) { 85 t.running.Store(true) 86 defer t.running.Store(false) 87 88 t.wg.Add(1) 89 defer t.wg.Done() 90 91 opts := docker_types.ContainerLogsOptions{ 92 ShowStdout: true, 93 ShowStderr: true, 94 Follow: true, 95 Timestamps: true, 96 Since: strconv.FormatInt(t.since, 10), 97 } 98 99 logs, err := t.client.ContainerLogs(ctx, t.containerName, opts) 100 if err != nil { 101 level.Error(t.logger).Log("msg", "could not fetch logs for container", "container", t.containerName, "err", err) 102 t.err = err 103 return 104 } 105 106 // Start transferring 107 rstdout, wstdout := io.Pipe() 108 rstderr, wstderr := io.Pipe() 109 t.wg.Add(1) 110 go func() { 111 defer func() { 112 t.wg.Done() 113 wstdout.Close() 114 wstderr.Close() 115 t.Stop() 116 }() 117 118 written, err := stdcopy.StdCopy(wstdout, wstderr, logs) 119 if err != nil { 120 level.Warn(t.logger).Log("msg", "could not transfer logs", "written", written, "container", t.containerName, "err", err) 121 } else { 122 level.Info(t.logger).Log("msg", "finished transferring logs", "written", written, "container", t.containerName) 123 } 124 }() 125 126 // Start processing 127 t.wg.Add(2) 128 go t.process(rstdout, "stdout") 129 go t.process(rstderr, "stderr") 130 131 // Wait until done 132 <-ctx.Done() 133 logs.Close() 134 level.Debug(t.logger).Log("msg", "done processing Docker logs", "container", t.containerName) 135 } 136 137 // extractTs tries for read the timestamp from the beginning of the log line. 138 // It's expected to follow the format 2006-01-02T15:04:05.999999999Z07:00. 139 func extractTs(line string) (time.Time, string, error) { 140 pair := strings.SplitN(line, " ", 2) 141 if len(pair) != 2 { 142 return time.Now(), line, fmt.Errorf("Could not find timestamp in '%s'", line) 143 } 144 ts, err := time.Parse("2006-01-02T15:04:05.999999999Z07:00", pair[0]) 145 if err != nil { 146 return time.Now(), line, fmt.Errorf("Could not parse timestamp from '%s': %w", pair[0], err) 147 } 148 return ts, pair[1], nil 149 } 150 151 // https://devmarkpro.com/working-big-files-golang 152 func readLine(r *bufio.Reader) (string, error) { 153 var ( 154 isPrefix = true 155 err error 156 line, ln []byte 157 ) 158 159 for isPrefix && err == nil { 160 line, isPrefix, err = r.ReadLine() 161 ln = append(ln, line...) 162 } 163 164 return string(ln), err 165 } 166 167 func (t *Target) process(r io.Reader, logStream string) { 168 defer func() { 169 t.wg.Done() 170 }() 171 172 reader := bufio.NewReader(r) 173 for { 174 line, err := readLine(reader) 175 if err != nil { 176 if err == io.EOF { 177 break 178 } 179 level.Error(t.logger).Log("msg", "error reading docker log line, skipping line", "err", err) 180 t.metrics.dockerErrors.Inc() 181 } 182 183 ts, line, err := extractTs(line) 184 if err != nil { 185 level.Error(t.logger).Log("msg", "could not extract timestamp, skipping line", "err", err) 186 t.metrics.dockerErrors.Inc() 187 continue 188 } 189 190 // Add all labels from the config, relabel and filter them. 191 lb := labels.NewBuilder(nil) 192 for k, v := range t.labels { 193 lb.Set(string(k), string(v)) 194 } 195 lb.Set(dockerLabelLogStream, logStream) 196 processed := relabel.Process(lb.Labels(), t.relabelConfig...) 197 198 filtered := make(model.LabelSet) 199 for _, lbl := range processed { 200 if strings.HasPrefix(lbl.Name, "__") { 201 continue 202 } 203 filtered[model.LabelName(lbl.Name)] = model.LabelValue(lbl.Value) 204 } 205 206 t.handler.Chan() <- api.Entry{ 207 Labels: filtered, 208 Entry: logproto.Entry{ 209 Timestamp: ts, 210 Line: line, 211 }, 212 } 213 t.metrics.dockerEntries.Inc() 214 t.positions.Put(positions.CursorKey(t.containerName), ts.Unix()) 215 } 216 } 217 218 // startIfNotRunning starts processing container logs. The operation is idempotent , i.e. the processing cannot be started twice. 219 func (t *Target) startIfNotRunning() { 220 if t.running.CAS(false, true) { 221 level.Debug(t.logger).Log("msg", "starting process loop", "container", t.containerName) 222 ctx, cancel := context.WithCancel(context.Background()) 223 t.cancel = cancel 224 go t.processLoop(ctx) 225 } else { 226 level.Debug(t.logger).Log("msg", "attempted to start process loop but it's already running", "container", t.containerName) 227 } 228 } 229 230 func (t *Target) Stop() { 231 t.cancel() 232 t.wg.Wait() 233 level.Debug(t.logger).Log("msg", "stopped Docker target", "container", t.containerName) 234 } 235 236 func (t *Target) Type() target.TargetType { 237 return target.DockerTargetType 238 } 239 240 func (t *Target) Ready() bool { 241 return t.running.Load() 242 } 243 244 func (t *Target) DiscoveredLabels() model.LabelSet { 245 return t.labels 246 } 247 248 func (t *Target) Labels() model.LabelSet { 249 return t.labels 250 } 251 252 // Details returns target-specific details. 253 func (t *Target) Details() interface{} { 254 return map[string]string{ 255 "id": t.containerName, 256 "error": t.err.Error(), 257 "position": t.positions.GetString(positions.CursorKey(t.containerName)), 258 "running": strconv.FormatBool(t.running.Load()), 259 } 260 }