input/otlp/exceptions.go (168 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.
// Portions copied from OpenTelemetry Collector (contrib), from the
// elastic exporter.
//
// Copyright 2020, OpenTelemetry Authors
//
// Licensed 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 otlp
import (
"bufio"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"github.com/elastic/apm-data/model/modelpb"
)
var (
javaStacktraceAtRegexp = regexp.MustCompile(`at (.*)\(([^:]*)(?::([0-9]+))?\)`)
javaStacktraceMoreRegexp = regexp.MustCompile(`\.\.\. ([0-9]+) more`)
)
const (
emptyExceptionMsg = "[EMPTY]"
)
// convertOpenTelemetryExceptionSpanEvent creates an otel Exception event
// from the specified arguments.
//
// OpenTelemetry semantic convention require the presence of at least one
// of the following attributes:
// - exception.type
// - exception.message
// https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#attributes
//
// To fulfill this requirement we do not set exception.type if empty but
// we always set exception.message defaulting to a constant value if empty.
func convertOpenTelemetryExceptionSpanEvent(
exceptionType, exceptionMessage, exceptionStacktrace string,
exceptionEscaped bool,
language string,
) *modelpb.Error {
if exceptionMessage == "" {
exceptionMessage = emptyExceptionMsg
}
exceptionHandled := !exceptionEscaped
exceptionError := modelpb.Error{}
exceptionError.Exception = &modelpb.Exception{}
exceptionError.Exception.Message = exceptionMessage
if exceptionType != "" {
exceptionError.Exception.Type = exceptionType
}
exceptionError.Exception.Handled = &exceptionHandled
if id, err := newUniqueID(); err == nil {
exceptionError.Id = id
}
if exceptionStacktrace != "" {
if err := setExceptionStacktrace(exceptionStacktrace, language, exceptionError.Exception); err != nil {
// Couldn't parse stacktrace, set it as `error.stack_trace` instead.
exceptionError.Exception.Stacktrace = nil
exceptionError.Exception.Cause = nil
exceptionError.StackTrace = exceptionStacktrace
}
}
return &exceptionError
}
func setExceptionStacktrace(s, language string, out *modelpb.Exception) error {
switch language {
case "java":
return setJavaExceptionStacktrace(s, out)
}
return fmt.Errorf("parsing %q stacktraces not implemented", language)
}
// setJavaExceptionStacktrace parses a Java exception stack trace according to
// https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#printStackTrace()
func setJavaExceptionStacktrace(s string, out *modelpb.Exception) error {
const (
causedByPrefix = "Caused by: "
suppressedPrefix = "Suppressed: "
)
type Exception struct {
*modelpb.Exception
enclosing *modelpb.Exception
indent int
}
first := true
current := Exception{out, nil, 0}
stack := []Exception{}
scanner := bufio.NewScanner(strings.NewReader(s))
for j := 0; scanner.Scan(); j++ {
if first {
// Ignore the first line, we only care about the locations.
first = false
continue
}
var indent int
line := scanner.Text()
if i := strings.IndexFunc(line, isNotTab); i > 0 {
line = line[i:]
indent = i
}
for indent < current.indent {
n := len(stack)
current, stack = stack[n-1], stack[:n-1]
}
switch {
case strings.HasPrefix(line, "at "):
submatch := javaStacktraceAtRegexp.FindStringSubmatch(line)
if submatch == nil {
return fmt.Errorf("failed to parse stacktrace at line %d", j)
}
parseJavaStacktraceFrame(submatch, current.Exception)
case strings.HasPrefix(line, "..."):
// "... N more" lines indicate that the last N frames from the enclosing
// exception's stacktrace are common to this exception.
if current.enclosing == nil {
return fmt.Errorf("no enclosing exception preceding at line %d", j)
}
submatch := javaStacktraceMoreRegexp.FindStringSubmatch(line)
if submatch == nil {
return fmt.Errorf("failed to parse stacktrace at line %d", j)
}
if n, err := strconv.Atoi(submatch[1]); err == nil {
enclosing := current.enclosing
if len(enclosing.Stacktrace) < n {
return fmt.Errorf(
"enclosing exception stacktrace has %d frames, cannot satisfy line %d",
len(enclosing.Stacktrace), j,
)
}
m := len(enclosing.Stacktrace)
current.Stacktrace = append(current.Stacktrace, enclosing.Stacktrace[m-n:]...)
}
case strings.HasPrefix(line, causedByPrefix):
// "Caused by:" lines are at the same level of indentation
// as the enclosing exception.
current.Cause = make([]*modelpb.Exception, 1)
current.Cause[0] = &modelpb.Exception{}
current.enclosing = current.Exception
current.Exception = current.Cause[0]
current.Exception.Handled = current.enclosing.Handled
current.Message = line[len(causedByPrefix):]
case strings.HasPrefix(line, suppressedPrefix):
// Suppressed exceptions have no place in the Elastic APM
// model, so they are ignored.
//
// Unlike "Caused by:", "Suppressed:" lines are indented within their
// enclosing exception; we just account for the indentation here.
stack = append(stack, current)
current.enclosing = current.Exception
current.Exception = &modelpb.Exception{}
current.indent = indent
default:
return fmt.Errorf("unexpected value at line %d", j)
}
}
return scanner.Err()
}
func parseJavaStacktraceFrame(submatch []string, out *modelpb.Exception) {
var module string
function := submatch[1]
if slash := strings.IndexRune(function, '/'); slash >= 0 {
// We could have either:
// - "class_loader/module/class.method"
// - "module/class.method"
module, function = function[:slash], function[slash+1:]
if slash := strings.IndexRune(function, '/'); slash >= 0 {
module, function = function[:slash], function[slash+1:]
}
}
var classname string
if dot := strings.LastIndexByte(function, '.'); dot > 0 {
// Split into classname and method.
classname, function = function[:dot], function[dot+1:]
}
file := submatch[2]
var lineno *uint32
if submatch[3] != "" {
if n, err := strconv.ParseUint(submatch[3], 10, 32); err == nil {
un := uint32(n)
lineno = &un
}
}
sf := modelpb.StacktraceFrame{}
sf.Module = module
sf.Classname = classname
sf.Function = function
sf.Filename = file
sf.Lineno = lineno
out.Stacktrace = append(out.Stacktrace, &sf)
}
func isNotTab(r rune) bool {
return r != '\t'
}
func newUniqueID() (string, error) {
var u [16]byte
if _, err := io.ReadFull(rand.Reader, u[:]); err != nil {
return "", err
}
// convert to string
buf := make([]byte, 32)
hex.Encode(buf, u[:])
return string(buf), nil
}