github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/logql/log/metrics_extraction.go (about) 1 package log 2 3 import ( 4 "sort" 5 "strconv" 6 "time" 7 8 "github.com/pkg/errors" 9 "github.com/prometheus/prometheus/model/labels" 10 11 "github.com/dustin/go-humanize" 12 ) 13 14 const ( 15 ConvertBytes = "bytes" 16 ConvertDuration = "duration" 17 ConvertFloat = "float" 18 ) 19 20 // LineExtractor extracts a float64 from a log line. 21 type LineExtractor func([]byte) float64 22 23 var ( 24 CountExtractor LineExtractor = func(line []byte) float64 { return 1. } 25 BytesExtractor LineExtractor = func(line []byte) float64 { return float64(len(line)) } 26 ) 27 28 // SampleExtractor creates StreamSampleExtractor that can extract samples for a given log stream. 29 type SampleExtractor interface { 30 ForStream(labels labels.Labels) StreamSampleExtractor 31 } 32 33 // StreamSampleExtractor extracts sample for a log line. 34 // A StreamSampleExtractor never mutate the received line. 35 type StreamSampleExtractor interface { 36 BaseLabels() LabelsResult 37 Process(ts int64, line []byte) (float64, LabelsResult, bool) 38 ProcessString(ts int64, line string) (float64, LabelsResult, bool) 39 } 40 41 type lineSampleExtractor struct { 42 Stage 43 LineExtractor 44 45 baseBuilder *BaseLabelsBuilder 46 streamExtractors map[uint64]StreamSampleExtractor 47 } 48 49 // NewLineSampleExtractor creates a SampleExtractor from a LineExtractor. 50 // Multiple log stages are run before converting the log line. 51 func NewLineSampleExtractor(ex LineExtractor, stages []Stage, groups []string, without, noLabels bool) (SampleExtractor, error) { 52 s := ReduceStages(stages) 53 hints := newParserHint(s.RequiredLabelNames(), groups, without, noLabels, "") 54 return &lineSampleExtractor{ 55 Stage: s, 56 LineExtractor: ex, 57 baseBuilder: NewBaseLabelsBuilderWithGrouping(groups, hints, without, noLabels), 58 streamExtractors: make(map[uint64]StreamSampleExtractor), 59 }, nil 60 } 61 62 func (l *lineSampleExtractor) ForStream(labels labels.Labels) StreamSampleExtractor { 63 hash := l.baseBuilder.Hash(labels) 64 if res, ok := l.streamExtractors[hash]; ok { 65 return res 66 } 67 68 res := &streamLineSampleExtractor{ 69 Stage: l.Stage, 70 LineExtractor: l.LineExtractor, 71 builder: l.baseBuilder.ForLabels(labels, hash), 72 } 73 l.streamExtractors[hash] = res 74 return res 75 } 76 77 type streamLineSampleExtractor struct { 78 Stage 79 LineExtractor 80 builder *LabelsBuilder 81 } 82 83 func (l *streamLineSampleExtractor) Process(ts int64, line []byte) (float64, LabelsResult, bool) { 84 // short circuit. 85 if l.Stage == NoopStage { 86 return l.LineExtractor(line), l.builder.GroupedLabels(), true 87 } 88 l.builder.Reset() 89 line, ok := l.Stage.Process(ts, line, l.builder) 90 if !ok { 91 return 0, nil, false 92 } 93 return l.LineExtractor(line), l.builder.GroupedLabels(), true 94 } 95 96 func (l *streamLineSampleExtractor) ProcessString(ts int64, line string) (float64, LabelsResult, bool) { 97 // unsafe get bytes since we have the guarantee that the line won't be mutated. 98 return l.Process(ts, unsafeGetBytes(line)) 99 } 100 101 func (l *streamLineSampleExtractor) BaseLabels() LabelsResult { return l.builder.currentResult } 102 103 type convertionFn func(value string) (float64, error) 104 105 type labelSampleExtractor struct { 106 preStage Stage 107 postFilter Stage 108 labelName string 109 conversionFn convertionFn 110 111 baseBuilder *BaseLabelsBuilder 112 streamExtractors map[uint64]StreamSampleExtractor 113 } 114 115 // LabelExtractorWithStages creates a SampleExtractor that will extract metrics from a labels. 116 // A set of log stage is executed before the conversion. A Filtering stage is executed after the conversion allowing 117 // to remove sample containing the __error__ label. 118 func LabelExtractorWithStages( 119 labelName, conversion string, 120 groups []string, without, noLabels bool, 121 preStages []Stage, 122 postFilter Stage, 123 ) (SampleExtractor, error) { 124 var convFn convertionFn 125 switch conversion { 126 case ConvertBytes: 127 convFn = convertBytes 128 case ConvertDuration: 129 convFn = convertDuration 130 case ConvertFloat: 131 convFn = convertFloat 132 default: 133 return nil, errors.Errorf("unsupported conversion operation %s", conversion) 134 } 135 if len(groups) == 0 || without { 136 without = true 137 groups = append(groups, labelName) 138 sort.Strings(groups) 139 } 140 preStage := ReduceStages(preStages) 141 hints := newParserHint(append(preStage.RequiredLabelNames(), postFilter.RequiredLabelNames()...), groups, without, noLabels, labelName) 142 return &labelSampleExtractor{ 143 preStage: preStage, 144 conversionFn: convFn, 145 labelName: labelName, 146 postFilter: postFilter, 147 baseBuilder: NewBaseLabelsBuilderWithGrouping(groups, hints, without, noLabels), 148 streamExtractors: make(map[uint64]StreamSampleExtractor), 149 }, nil 150 } 151 152 type streamLabelSampleExtractor struct { 153 *labelSampleExtractor 154 builder *LabelsBuilder 155 } 156 157 func (l *labelSampleExtractor) ForStream(labels labels.Labels) StreamSampleExtractor { 158 hash := l.baseBuilder.Hash(labels) 159 if res, ok := l.streamExtractors[hash]; ok { 160 return res 161 } 162 163 res := &streamLabelSampleExtractor{ 164 labelSampleExtractor: l, 165 builder: l.baseBuilder.ForLabels(labels, hash), 166 } 167 l.streamExtractors[hash] = res 168 return res 169 } 170 171 func (l *streamLabelSampleExtractor) Process(ts int64, line []byte) (float64, LabelsResult, bool) { 172 // Apply the pipeline first. 173 l.builder.Reset() 174 line, ok := l.preStage.Process(ts, line, l.builder) 175 if !ok { 176 return 0, nil, false 177 } 178 // convert the label value. 179 var v float64 180 stringValue, _ := l.builder.Get(l.labelName) 181 if stringValue == "" { 182 // NOTE: It's totally fine for log line to not have this particular label. 183 // See Issue: https://github.com/grafana/loki/issues/6713 184 return 0, nil, false 185 } 186 187 var err error 188 v, err = l.conversionFn(stringValue) 189 if err != nil { 190 l.builder.SetErr(errSampleExtraction) 191 l.builder.SetErrorDetails(err.Error()) 192 } 193 194 // post filters 195 if _, ok = l.postFilter.Process(ts, line, l.builder); !ok { 196 return 0, nil, false 197 } 198 return v, l.builder.GroupedLabels(), true 199 } 200 201 func (l *streamLabelSampleExtractor) ProcessString(ts int64, line string) (float64, LabelsResult, bool) { 202 // unsafe get bytes since we have the guarantee that the line won't be mutated. 203 return l.Process(ts, unsafeGetBytes(line)) 204 } 205 206 func (l *streamLabelSampleExtractor) BaseLabels() LabelsResult { return l.builder.currentResult } 207 208 // NewFilteringSampleExtractor creates a sample extractor where entries from 209 // the underlying log stream are filtered by pipeline filters before being 210 // passed to extract samples. Filters are always upstream of the extractor. 211 func NewFilteringSampleExtractor(f []PipelineFilter, e SampleExtractor) SampleExtractor { 212 return &filteringSampleExtractor{ 213 filters: f, 214 extractor: e, 215 } 216 } 217 218 type filteringSampleExtractor struct { 219 filters []PipelineFilter 220 extractor SampleExtractor 221 } 222 223 func (p *filteringSampleExtractor) ForStream(labels labels.Labels) StreamSampleExtractor { 224 var streamFilters []streamFilter 225 for _, f := range p.filters { 226 if allMatch(f.Matchers, labels) { 227 streamFilters = append(streamFilters, streamFilter{ 228 start: f.Start, 229 end: f.End, 230 pipeline: f.Pipeline.ForStream(labels), 231 }) 232 } 233 } 234 235 return &filteringStreamExtractor{ 236 filters: streamFilters, 237 extractor: p.extractor.ForStream(labels), 238 } 239 } 240 241 type filteringStreamExtractor struct { 242 filters []streamFilter 243 extractor StreamSampleExtractor 244 } 245 246 func (sp *filteringStreamExtractor) BaseLabels() LabelsResult { 247 return sp.extractor.BaseLabels() 248 } 249 250 func (sp *filteringStreamExtractor) Process(ts int64, line []byte) (float64, LabelsResult, bool) { 251 for _, filter := range sp.filters { 252 if ts < filter.start || ts > filter.end { 253 continue 254 } 255 256 _, _, matches := filter.pipeline.Process(ts, line) 257 if matches { //When the filter matches, don't run the next step 258 return 0, nil, false 259 } 260 } 261 262 return sp.extractor.Process(ts, line) 263 } 264 265 func (sp *filteringStreamExtractor) ProcessString(ts int64, line string) (float64, LabelsResult, bool) { 266 for _, filter := range sp.filters { 267 if ts < filter.start || ts > filter.end { 268 continue 269 } 270 271 _, _, matches := filter.pipeline.ProcessString(ts, line) 272 if matches { //When the filter matches, don't run the next step 273 return 0, nil, false 274 } 275 } 276 277 return sp.extractor.ProcessString(ts, line) 278 } 279 280 func convertFloat(v string) (float64, error) { 281 return strconv.ParseFloat(v, 64) 282 } 283 284 func convertDuration(v string) (float64, error) { 285 d, err := time.ParseDuration(v) 286 if err != nil { 287 return 0, err 288 } 289 return d.Seconds(), nil 290 } 291 292 func convertBytes(v string) (float64, error) { 293 b, err := humanize.ParseBytes(v) 294 if err != nil { 295 return 0, err 296 } 297 return float64(b), nil 298 }