model/modelprocessor/groupingkey.go (100 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package modelprocessor
import (
"context"
"encoding/hex"
"hash"
"io"
"github.com/elastic/apm-data/model/modelpb"
)
// SetGroupingKey is a modelpb.BatchProcessor that sets the grouping key for errors
// by hashing their stack frames.
type SetGroupingKey struct {
NewHash func() hash.Hash
}
// ProcessBatch sets the grouping key for errors.
func (s SetGroupingKey) ProcessBatch(ctx context.Context, b *modelpb.Batch) error {
for _, event := range *b {
if event.Error != nil {
s.processError(ctx, event.Error)
}
}
return nil
}
func (s SetGroupingKey) processError(ctx context.Context, event *modelpb.Error) {
hash := s.NewHash()
var updated bool
if event.Exception != nil {
if s.hashExceptionTree(event.Exception, hash, s.hashExceptionType) {
updated = true
}
}
if event.Log != nil {
if s.maybeWriteString(event.Log.ParamMessage, hash) {
updated = true
}
}
var haveExceptionStacktrace bool
if event.Exception != nil {
haveExceptionStacktrace = s.hashExceptionTree(event.Exception, hash, s.hashExceptionStacktrace)
updated = updated || haveExceptionStacktrace
}
if !haveExceptionStacktrace && event.Log != nil {
if s.hashStacktrace(event.Log.Stacktrace, hash) {
updated = true
}
}
if !updated {
if event.Exception != nil {
updated = s.hashExceptionTree(event.Exception, hash, s.hashExceptionMessage)
}
if !updated && event.Log != nil {
s.maybeWriteString(event.Log.Message, hash)
}
}
event.GroupingKey = hex.EncodeToString(hash.Sum(nil))
}
func (s SetGroupingKey) hashExceptionTree(e *modelpb.Exception, out hash.Hash, f func(*modelpb.Exception, hash.Hash) bool) bool {
updated := f(e, out)
for _, cause := range e.Cause {
if s.hashExceptionTree(cause, out, f) {
updated = true
}
}
return updated
}
func (s SetGroupingKey) hashExceptionType(e *modelpb.Exception, out hash.Hash) bool {
return s.maybeWriteString(e.Type, out)
}
func (s SetGroupingKey) hashExceptionMessage(e *modelpb.Exception, out hash.Hash) bool {
return s.maybeWriteString(e.Message, out)
}
func (s SetGroupingKey) hashExceptionStacktrace(e *modelpb.Exception, out hash.Hash) bool {
return s.hashStacktrace(e.Stacktrace, out)
}
func (s SetGroupingKey) hashStacktrace(stacktrace []*modelpb.StacktraceFrame, out hash.Hash) bool {
var updated bool
for _, frame := range stacktrace {
if frame.ExcludeFromGrouping {
continue
}
switch {
case frame.Module != "":
io.WriteString(out, frame.Module)
updated = true
case frame.Filename != "":
io.WriteString(out, frame.Filename)
updated = true
case frame.Classname != "":
io.WriteString(out, frame.Classname)
updated = true
}
if s.maybeWriteString(frame.Function, out) {
updated = true
}
}
return updated
}
func (SetGroupingKey) maybeWriteString(s string, out hash.Hash) bool {
if s == "" {
return false
}
io.WriteString(out, s)
return true
}