Skip to content

Commit c390b57

Browse files
author
Igor Komlew
authored
* Added sentry reports as an extra middlware (#316)
* Added some additional headers to the report filter * Added panic handler, added sentry filter for the events
1 parent 377493c commit c390b57

File tree

11 files changed

+556
-9
lines changed

11 files changed

+556
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ files/
2727
/env
2828
/dist
2929
/tmp_charm
30+
.vscode/

charm/serial-vault/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ options:
7171
description: "Factory sync only"
7272
type: string
7373
default: ""
74+
sentryDSN:
75+
description: "Sentry data source name"
76+
type: string
77+
default: ""

charm/serial-vault/templates/settings.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ jwtSecret: "{{ jwtSecret }}"
1616
syncUrl: "{{ syncUrl }}"
1717
syncUser: "{{ syncUser }}"
1818
syncAPIKey: "{{ syncAPIKey }}"
19+
sentryDSN: "{{ sentryDSN }}"

cmd/serial-vault/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@ import (
2727
"github.com/CanonicalLtd/serial-vault/datastore"
2828
"github.com/CanonicalLtd/serial-vault/service"
2929
svlog "github.com/CanonicalLtd/serial-vault/service/log"
30+
"github.com/CanonicalLtd/serial-vault/service/sentry"
3031
logging "github.com/op/go-logging"
3132
)
3233

33-
func init() {
34+
const appName = "serialvault"
35+
36+
func initLogger() {
37+
err := sentry.Init(datastore.Environ.Config.SentryDSN, appName, datastore.Environ.Config.Version)
38+
if err != nil {
39+
svlog.Errorf("sentry init error: %v", err)
40+
}
3441
svlog.InitLogger(logging.INFO)
3542
}
3643

@@ -42,6 +49,7 @@ func main() {
4249
if err != nil {
4350
svlog.Fatalf("Error parsing the config file: %v", err)
4451
}
52+
initLogger()
4553

4654
// Open the connection to the local database
4755
datastore.OpenSysDatabase(datastore.Environ.Config.Driver, datastore.Environ.Config.DataSource)

config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package config
2222
import (
2323
"flag"
2424
"io/ioutil"
25+
"os"
2526

2627
"github.com/CanonicalLtd/serial-vault/service/log"
2728

@@ -57,6 +58,7 @@ type Settings struct {
5758
SyncURL string `yaml:"syncUrl"`
5859
SyncUser string `yaml:"syncUser"`
5960
SyncAPIKey string `yaml:"syncAPIKey"`
61+
SentryDSN string `yaml:"sentryDSN"`
6062
}
6163

6264
// SettingsFile is the path to the YAML configuration file
@@ -95,5 +97,9 @@ func ReadConfig(settings *Settings, filePath string) error {
9597
ServiceMode = settings.Mode
9698
}
9799

100+
if settings.SentryDSN == "" {
101+
settings.SentryDSN = os.Getenv("SENTRY_DSN")
102+
}
103+
98104
return nil
99105
}

go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.13
55
require (
66
github.com/Masterminds/squirrel v1.2.0
77
github.com/dgrijalva/jwt-go v3.2.0+incompatible
8+
github.com/getsentry/sentry-go v0.7.0
89
github.com/godbus/dbus v4.1.0+incompatible // indirect
910
github.com/gorilla/csrf v1.0.3-0.20161122164500-69581736821c
1011
github.com/gorilla/mux v1.6.1
@@ -18,20 +19,19 @@ require (
1819
github.com/mattn/go-sqlite3 v1.6.0
1920
github.com/ojii/gettext.go v0.0.0-20170120061437-b6dae1d7af8a
2021
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
21-
github.com/pkg/errors v0.8.1-0.20180311214515-816c9085562c
22+
github.com/pkg/errors v0.8.1
2223
github.com/prometheus/client_golang v1.1.0
2324
github.com/snapcore/bolt v1.3.1 // indirect
2425
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
2526
github.com/snapcore/snapd v0.0.0-20200317200833-16631e228c07
2627
github.com/yohcop/openid-go v0.0.0-20170901155220-cfc72ed89575
27-
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
28-
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
29-
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3
30-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
28+
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
29+
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
30+
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a
3131
gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
3232
gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5
3333
gopkg.in/macaroon.v1 v1.0.0-20170816141150-ab101776739e
3434
gopkg.in/retry.v1 v1.0.0
3535
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
36-
gopkg.in/yaml.v2 v2.2.1
36+
gopkg.in/yaml.v2 v2.2.4
3737
)

go.sum

Lines changed: 160 additions & 0 deletions
Large diffs are not rendered by default.

service/middleware.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
package service
2121

2222
import (
23+
"context"
2324
"encoding/json"
25+
"runtime/debug"
2426

2527
"net/http"
2628
"os"
@@ -29,6 +31,8 @@ import (
2931
"github.com/CanonicalLtd/serial-vault/datastore"
3032
"github.com/CanonicalLtd/serial-vault/service/log"
3133
"github.com/CanonicalLtd/serial-vault/service/response"
34+
servicesentry "github.com/CanonicalLtd/serial-vault/service/sentry"
35+
"github.com/getsentry/sentry-go"
3236
"github.com/gorilla/csrf"
3337
)
3438

@@ -38,14 +42,18 @@ func Logger(start time.Time, r *http.Request) {
3842
}
3943

4044
// ErrorHandler is a standard error handler middleware that generates the error response
45+
// and sentry reports
4146
func ErrorHandler(f func(http.ResponseWriter, *http.Request) response.ErrorResponse) http.HandlerFunc {
4247
return func(w http.ResponseWriter, r *http.Request) {
48+
ctx := r.Context()
4349
// Call the handler and it will return a custom error
4450
e := f(w, r)
4551
if !e.Success {
4652
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
4753
w.WriteHeader(e.StatusCode)
4854

55+
servicesentry.Report(ctx, e)
56+
4957
// Encode the response as JSON
5058
if err := json.NewEncoder(w).Encode(e); err != nil {
5159
log.Printf("Error forming the signing response: %v\n", err)
@@ -58,11 +66,17 @@ func ErrorHandler(f func(http.ResponseWriter, *http.Request) response.ErrorRespo
5866
func Middleware(inner http.Handler) http.Handler {
5967
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6068
start := time.Now()
61-
69+
ctx := r.Context()
70+
hub := sentry.CurrentHub().Clone()
71+
// add the current request to the sentry scope
72+
// it will be automatically added to the report
73+
hub.Scope().SetRequest(r)
74+
ctx = sentry.SetHubOnContext(ctx, hub)
75+
defer recoverWithSentry(hub, w, r)
6276
// Log the request
6377
Logger(start, r)
6478

65-
inner.ServeHTTP(w, r)
79+
inner.ServeHTTP(w, r.WithContext(ctx))
6680
})
6781
}
6882

@@ -83,3 +97,24 @@ var MiddlewareWithCSRF = func(inner http.Handler) http.Handler {
8397

8498
return CSRF(Middleware(inner))
8599
}
100+
101+
func recoverWithSentry(hub *sentry.Hub, w http.ResponseWriter, r *http.Request) {
102+
if err := recover(); err != nil {
103+
log.Errorf("recover from panic: %v", err)
104+
debug.PrintStack()
105+
106+
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
107+
e := response.ErrorInternal
108+
w.WriteHeader(e.StatusCode)
109+
110+
// Encode the response as JSON
111+
if err := json.NewEncoder(w).Encode(e); err != nil {
112+
log.Printf("Error forming the error response after recovering from panic: %v\n", err)
113+
}
114+
115+
hub.RecoverWithContext(
116+
context.WithValue(r.Context(), sentry.RequestContextKey, r),
117+
err,
118+
)
119+
}
120+
}

service/response/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ var (
6565
ErrorAccountAssertion = ErrorResponse{false, "account-assertion", "", "Error retrieving the account assertion from the database", http.StatusBadRequest}
6666
ErrorSignAssertion = ErrorResponse{false, "signing-assertion", "", "Error signing the assertion", http.StatusBadRequest}
6767
ErrorGenerateNonce = ErrorResponse{false, "generate-nonce", "", "Error generating a nonce. Please try again later", http.StatusBadRequest}
68+
ErrorInternal = ErrorResponse{false, "server-error", "", "Internal Server Error", http.StatusInternalServerError}
6869
)

service/sentry/sentry.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package sentry
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"time"
9+
10+
"github.com/CanonicalLtd/serial-vault/service/response"
11+
"github.com/getsentry/sentry-go"
12+
)
13+
14+
const placeholder = "[Filtered]"
15+
16+
var headerFilter = []string{"Api-Key", "Authorization", "User", "Cookie"}
17+
18+
// Init initializes sentry client
19+
func Init(dsn, serviceName, serviceVersion string) error {
20+
sentrySyncTransport := sentry.NewHTTPSyncTransport()
21+
sentrySyncTransport.Timeout = time.Second * 3
22+
23+
opt := sentry.ClientOptions{
24+
Dsn: dsn,
25+
Release: serviceVersion,
26+
Transport: sentrySyncTransport,
27+
AttachStacktrace: true,
28+
BeforeSend: getFilteredEvent,
29+
}
30+
if err := sentry.Init(opt); err != nil {
31+
return err
32+
}
33+
34+
sentry.ConfigureScope(func(scope *sentry.Scope) {
35+
scope.SetTag("app_name", serviceName)
36+
})
37+
return nil
38+
}
39+
40+
// Report creates sentry report from response.ErrorResponse
41+
func Report(ctx context.Context, resp response.ErrorResponse) {
42+
if !shouldReport(resp) {
43+
return
44+
}
45+
46+
if hub := sentry.GetHubFromContext(ctx); hub != nil {
47+
go hub.WithScope(func(scope *sentry.Scope) {
48+
// create a sentry report
49+
scope.SetTag("http_status_code", fmt.Sprint(resp.StatusCode))
50+
scope.SetTag("error_code", resp.Code)
51+
if resp.SubCode != "" {
52+
scope.SetTag("error_subcode", resp.SubCode)
53+
}
54+
scope.SetLevel(sentry.LevelError)
55+
hub.CaptureMessage(resp.Message)
56+
})
57+
}
58+
}
59+
60+
func getFilteredEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
61+
for _, headerName := range headerFilter {
62+
if _, ok := event.Request.Headers[headerName]; ok {
63+
event.Request.Headers[headerName] = placeholder
64+
}
65+
}
66+
67+
if event.Request.QueryString != "" {
68+
event.Request.QueryString = GetFilteredQueryString(event.Request.QueryString)
69+
}
70+
71+
if event.Request.Data != "" {
72+
event.Request.Data = placeholder
73+
}
74+
75+
if event.Request.Cookies != "" {
76+
event.Request.Cookies = GetFilteredCookies(event.Request.Cookies)
77+
}
78+
79+
return event
80+
}
81+
82+
func shouldReport(resp response.ErrorResponse) bool {
83+
return resp.StatusCode >= 500
84+
}
85+
86+
// GetFilteredQueryString replaces all the values in the query string
87+
func GetFilteredQueryString(q string) string {
88+
values, err := url.ParseQuery(q)
89+
if err != nil {
90+
return placeholder
91+
}
92+
93+
for name := range values {
94+
values.Set(name, placeholder)
95+
}
96+
97+
return values.Encode()
98+
}
99+
100+
// GetFilteredCookies replaces all the values in the cookie string
101+
func GetFilteredCookies(rawCookies string) string {
102+
header := http.Header{}
103+
header.Add("Cookie", rawCookies)
104+
request := &http.Request{Header: header}
105+
106+
var newCookies string
107+
for _, c := range request.Cookies() {
108+
s := fmt.Sprintf("%s=%s", c.Name, placeholder)
109+
if newCookies != "" {
110+
newCookies = newCookies + "; " + s
111+
} else {
112+
newCookies = s
113+
}
114+
115+
}
116+
return newCookies
117+
}

0 commit comments

Comments
 (0)