Skip to content

Commit

Permalink
feat(api): Building out a sourcemap system for sentry (#1736)
Browse files Browse the repository at this point in the history
* feat(api): Building out a sourcemap system for sentry

I want to be able to bundle the golang source code in the sentry SDK at
build time. This is progress towards that.

* feat(api): Getting contextify and source reader in.
  • Loading branch information
elliotcourant committed Apr 12, 2024
1 parent fccc016 commit 807e455
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 114 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ server/ui/static/*
server/icons/sources/*
!server/icons/sources/simple-icons
!server/icons/sources/README.md
server/internal/source/embed/*
!server/internal/source/embed/README.md
tmp
local
values.local.yaml
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ clean:
-cmake --build $(CMAKE_CONFIGURATION_DIRECTORY) -t clean $(BUILD_ARGS)
-cmake -E remove_directory $(CMAKE_CONFIGURATION_DIRECTORY) $(BUILD_ARGS)
-git clean -f -X server/ui/static
-git clean -f -X server/internal/source/embed
-git submodule deinit -f server/icons/sources/simple-icons
-git submodule deinit -f server/legal/data/legal

Expand Down
1 change: 1 addition & 0 deletions cmake/backend.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ add_custom_command(
build.interface
build.email
download.legal
sourcemaps.golang
)

if(BUILD_SIMPLE_ICONS)
Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/elliotcourant/gofx v0.0.1
github.com/fsnotify/fsnotify v1.6.0
github.com/gavv/httpexpect/v2 v2.16.0
github.com/getsentry/sentry-go v0.21.0
github.com/getsentry/sentry-go v0.27.0
github.com/go-pg/migrations/v8 v8.1.0
github.com/go-pg/pg/v10 v10.12.0
github.com/gocraft/work v0.5.1
Expand All @@ -42,6 +42,7 @@ require (
github.com/stripe/stripe-go/v72 v72.122.0
github.com/teambition/rrule-go v1.8.2
github.com/vmihailenco/msgpack/v5 v5.3.5
github.com/wneessen/go-mail v0.4.1
github.com/xlzd/gotp v0.0.0-20220110052318-fab697c03c2c
golang.org/x/crypto v0.19.0
google.golang.org/api v0.169.0
Expand All @@ -55,13 +56,11 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/wneessen/go-mail v0.4.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
)

require (
Expand Down
113 changes: 4 additions & 109 deletions go.sum

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions server/background/gocraft.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (g *GoCraftWorkJobProcessor) RegisterJob(ctx context.Context, handler JobHa
// We want to have sentry tracking jobs as they are being processed. In order to do this we need to inject a
// sentry hub into the context and create a new span using that context.
highContext := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone())
span := sentry.StartSpan(highContext, "topic.process", sentry.TransactionName(handler.QueueName()))
span := sentry.StartSpan(highContext, "topic.process", sentry.WithTransactionName(handler.QueueName()))
span.Description = handler.QueueName()
jobLog := log.WithContext(span.Context())
hub := sentry.GetHubFromContext(span.Context())
Expand Down Expand Up @@ -206,7 +206,7 @@ func (g *GoCraftWorkJobProcessor) RegisterJob(ctx context.Context, handler JobHa
span := sentry.StartSpan(
context.Background(),
"topic.process",
sentry.TransactionName(schedulerName),
sentry.WithTransactionName(schedulerName),
)
defer span.Finish()
return scheduledJob.EnqueueTriggeredJob(span.Context(), g.enqueuer)
Expand Down
5 changes: 5 additions & 0 deletions server/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/monetr/monetr/server/cache"
"github.com/monetr/monetr/server/communication"
"github.com/monetr/monetr/server/config"
"github.com/monetr/monetr/server/internal/source"
"github.com/monetr/monetr/server/logging"
"github.com/monetr/monetr/server/metrics"
"github.com/monetr/monetr/server/platypus"
Expand Down Expand Up @@ -105,6 +106,10 @@ func RunServer() error {
SampleRate: configuration.Sentry.SampleRate,
EnableTracing: configuration.Sentry.TraceSampleRate > 0,
TracesSampleRate: configuration.Sentry.TraceSampleRate,
Integrations: func(i []sentry.Integration) []sentry.Integration {
// Add our own contextify frames integration
return append(i, new(source.ContextifyFramesIntegration))
},
BeforeSend: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
// Make sure user authentication doesn't make its way into sentry.
if event.Request != nil {
Expand Down
36 changes: 36 additions & 0 deletions server/internal/source/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@


set(GO_SRC_DIR "${CMAKE_SOURCE_DIR}/server")
file(GLOB_RECURSE APP_GO_FILES
"${GO_SRC_DIR}/*.go"
)
list(FILTER APP_GO_FILES EXCLUDE REGEX ".+_test\\.go")
list(FILTER APP_GO_FILES EXCLUDE REGEX "source/embed")

set(SOURCEMAP_MARKER ${CMAKE_BINARY_DIR}/golang-sourcemap.txt)
add_custom_command(
OUTPUT ${SOURCEMAP_MARKER}
COMMENT "Finished generating source maps for sentry from golang"
COMMAND ${CMAKE_COMMAND} -E touch ${SOURCEMAP_MARKER}
)

add_custom_target(
sourcemaps.golang
DEPENDS ${SOURCEMAP_MARKER}
)

foreach(FILE ${APP_GO_FILES})
string(REPLACE "${CMAKE_SOURCE_DIR}/" "${CMAKE_CURRENT_SOURCE_DIR}/embed/" OUTPUT_GO_FILE "${FILE}")
string(REPLACE "${CMAKE_SOURCE_DIR}/" "" FILE_FRIENDLY "${FILE}")
get_filename_component(OUTPUT_GO_DIRECTORY ${OUTPUT_GO_FILE} DIRECTORY)
add_custom_command(
OUTPUT ${OUTPUT_GO_FILE}.txt
BYPRODUCTS ${OUTPUT_GO_FILE}.txt
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTPUT_GO_DIRECTORY}
COMMAND ${CMAKE_COMMAND} -E copy ${FILE} ${OUTPUT_GO_FILE}.txt
COMMENT "Generating source map for ${FILE_FRIENDLY}"
DEPENDS ${FILE}
)
add_custom_command(OUTPUT ${SOURCEMAP_MARKER} APPEND DEPENDS ${OUTPUT_GO_FILE}.txt)
endforeach()

118 changes: 118 additions & 0 deletions server/internal/source/contextify_frames.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
FROM: https://github.com/getsentry/sentry-go/blob/df20ce63bbede6de539d2ad2f15f7daf2b703c65/integrations.go#L215-L328
MIT License
Copyright (c) 2019 Functional Software, Inc. dba Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package source

import (
"sync"

"github.com/getsentry/sentry-go"
)

// ================================
// Contextify Frames Integration
// ================================

type ContextifyFramesIntegration struct {
sr sourceReader
contextLines int
cachedLocations sync.Map
}

func (cfi *ContextifyFramesIntegration) Name() string {
return "ContextifyFrames"
}

func (cfi *ContextifyFramesIntegration) SetupOnce(client *sentry.Client) {
cfi.sr = newSourceReader()
cfi.contextLines = 5

client.AddEventProcessor(cfi.processor)
}

func (cfi *ContextifyFramesIntegration) processor(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
// Range over all exceptions
for _, ex := range event.Exception {
// If it has no stacktrace, just bail out
if ex.Stacktrace == nil {
continue
}

// If it does, it should have frames, so try to contextify them
ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames)
}

// Range over all threads
for _, th := range event.Threads {
// If it has no stacktrace, just bail out
if th.Stacktrace == nil {
continue
}

// If it does, it should have frames, so try to contextify them
th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames)
}

return event
}

func (cfi *ContextifyFramesIntegration) contextify(frames []sentry.Frame) []sentry.Frame {
contextifiedFrames := make([]sentry.Frame, 0, len(frames))

for _, frame := range frames {
if !frame.InApp {
contextifiedFrames = append(contextifiedFrames, frame)
continue
}

path := frame.AbsPath
if path == "" {
contextifiedFrames = append(contextifiedFrames, frame)
continue
}

lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines)
contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine))
}

return contextifiedFrames
}

func (cfi *ContextifyFramesIntegration) addContextLinesToFrame(frame sentry.Frame, lines [][]byte, contextLine int) sentry.Frame {
for i, line := range lines {
switch {
case i < contextLine:
frame.PreContext = append(frame.PreContext, string(line))
case i == contextLine:
frame.ContextLine = string(line)
default:
frame.PostContext = append(frame.PostContext, string(line))
}
}
return frame
}
3 changes: 3 additions & 0 deletions server/internal/source/embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Embedded Sources

This directory contains embedded source code from the build.
27 changes: 27 additions & 0 deletions server/internal/source/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package source

import (
"embed"
"fmt"
"runtime"
"strings"
)

//go:embed embed/**
var sourceCode embed.FS

var basePath string

func init() {
pc, _, _, ok := runtime.Caller(0)
details := runtime.FuncForPC(pc)
if ok && details != nil {
path, _ := details.FileLine(pc)
basePath = strings.TrimSuffix(path, "/server/internal/source/source.go")
}
}

func transformFileName(path string) string {
newPath := strings.TrimPrefix(path, basePath)
return fmt.Sprintf("/embed/%s.txt", newPath)
}
97 changes: 97 additions & 0 deletions server/internal/source/source_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
FROM: https://github.com/getsentry/sentry-go/blob/df20ce63bbede6de539d2ad2f15f7daf2b703c65/sourcereader.go
MIT License
Copyright (c) 2019 Functional Software, Inc. dba Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package source

import (
"bytes"
"sync"
)

type sourceReader struct {
mu sync.Mutex
cache map[string][][]byte
}

func newSourceReader() sourceReader {
return sourceReader{
cache: make(map[string][][]byte),
}
}

func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) {
sr.mu.Lock()
defer sr.mu.Unlock()

lines, ok := sr.cache[filename]

if !ok {
data, err := sourceCode.ReadFile(transformFileName(filename))
if err != nil {
sr.cache[filename] = nil
return nil, 0
}
lines = bytes.Split(data, []byte{'\n'})
sr.cache[filename] = lines
}

return sr.calculateContextLines(lines, line, context)
}

func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) {
// Stacktrace lines are 1-indexed, slices are 0-indexed
line--

// contextLine points to a line that caused an issue itself, in relation to
// returned slice.
contextLine := context

if lines == nil || line >= len(lines) || line < 0 {
return nil, 0
}

if context < 0 {
context = 0
contextLine = 0
}

start := line - context

if start < 0 {
contextLine += start
start = 0
}

end := line + context + 1

if end > len(lines) {
end = len(lines)
}

return lines[start:end], contextLine
}

0 comments on commit 807e455

Please sign in to comment.