286 строки
10 KiB
Go
286 строки
10 KiB
Go
package sentry
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Protocol Docs (kinda)
|
|
// https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs
|
|
|
|
// transactionType is the type of a transaction event.
|
|
const transactionType = "transaction"
|
|
|
|
// Level marks the severity of the event.
|
|
type Level string
|
|
|
|
// Describes the severity of the event.
|
|
const (
|
|
LevelDebug Level = "debug"
|
|
LevelInfo Level = "info"
|
|
LevelWarning Level = "warning"
|
|
LevelError Level = "error"
|
|
LevelFatal Level = "fatal"
|
|
)
|
|
|
|
// SdkInfo contains all metadata about about the SDK being used.
|
|
type SdkInfo struct {
|
|
Name string `json:"name,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
Integrations []string `json:"integrations,omitempty"`
|
|
Packages []SdkPackage `json:"packages,omitempty"`
|
|
}
|
|
|
|
// SdkPackage describes a package that was installed.
|
|
type SdkPackage struct {
|
|
Name string `json:"name,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
}
|
|
|
|
// TODO: This type could be more useful, as map of interface{} is too generic
|
|
// and requires a lot of type assertions in beforeBreadcrumb calls
|
|
// plus it could just be map[string]interface{} then.
|
|
|
|
// BreadcrumbHint contains information that can be associated with a Breadcrumb.
|
|
type BreadcrumbHint map[string]interface{}
|
|
|
|
// Breadcrumb specifies an application event that occurred before a Sentry event.
|
|
// An event may contain one or more breadcrumbs.
|
|
type Breadcrumb struct {
|
|
Category string `json:"category,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Level Level `json:"level,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// MarshalJSON converts the Breadcrumb struct to JSON.
|
|
func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
|
|
type alias Breadcrumb
|
|
// encoding/json doesn't support the "omitempty" option for struct types.
|
|
// See https://golang.org/issues/11939.
|
|
// This implementation of MarshalJSON shadows the original Timestamp field
|
|
// forcing it to be omitted when the Timestamp is the zero value of
|
|
// time.Time.
|
|
if b.Timestamp.IsZero() {
|
|
return json.Marshal(&struct {
|
|
*alias
|
|
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
|
}{
|
|
alias: (*alias)(b),
|
|
})
|
|
}
|
|
return json.Marshal((*alias)(b))
|
|
}
|
|
|
|
// User describes the user associated with an Event. If this is used, at least
|
|
// an ID or an IP address should be provided.
|
|
type User struct {
|
|
Email string `json:"email,omitempty"`
|
|
ID string `json:"id,omitempty"`
|
|
IPAddress string `json:"ip_address,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
}
|
|
|
|
// Request contains information on a HTTP request related to the event.
|
|
type Request struct {
|
|
URL string `json:"url,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
Data string `json:"data,omitempty"`
|
|
QueryString string `json:"query_string,omitempty"`
|
|
Cookies string `json:"cookies,omitempty"`
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
Env map[string]string `json:"env,omitempty"`
|
|
}
|
|
|
|
// NewRequest returns a new Sentry Request from the given http.Request.
|
|
//
|
|
// NewRequest avoids operations that depend on network access. In particular, it
|
|
// does not read r.Body.
|
|
func NewRequest(r *http.Request) *Request {
|
|
protocol := schemeHTTP
|
|
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
|
protocol = schemeHTTPS
|
|
}
|
|
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
|
|
|
|
// We read only the first Cookie header because of the specification:
|
|
// https://tools.ietf.org/html/rfc6265#section-5.4
|
|
// When the user agent generates an HTTP request, the user agent MUST NOT
|
|
// attach more than one Cookie header field.
|
|
cookies := r.Header.Get("Cookie")
|
|
|
|
headers := make(map[string]string, len(r.Header))
|
|
for k, v := range r.Header {
|
|
headers[k] = strings.Join(v, ",")
|
|
}
|
|
headers["Host"] = r.Host
|
|
|
|
var env map[string]string
|
|
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
|
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
|
}
|
|
|
|
return &Request{
|
|
URL: url,
|
|
Method: r.Method,
|
|
QueryString: r.URL.RawQuery,
|
|
Cookies: cookies,
|
|
Headers: headers,
|
|
Env: env,
|
|
}
|
|
}
|
|
|
|
// Exception specifies an error that occurred.
|
|
type Exception struct {
|
|
Type string `json:"type,omitempty"`
|
|
Value string `json:"value,omitempty"`
|
|
Module string `json:"module,omitempty"`
|
|
ThreadID string `json:"thread_id,omitempty"`
|
|
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
|
RawStacktrace *Stacktrace `json:"raw_stacktrace,omitempty"`
|
|
}
|
|
|
|
// EventID is a hexadecimal string representing a unique uuid4 for an Event.
|
|
// An EventID must be 32 characters long, lowercase and not have any dashes.
|
|
type EventID string
|
|
|
|
// Event is the fundamental data structure that is sent to Sentry.
|
|
type Event struct {
|
|
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
|
|
Contexts map[string]interface{} `json:"contexts,omitempty"`
|
|
Dist string `json:"dist,omitempty"`
|
|
Environment string `json:"environment,omitempty"`
|
|
EventID EventID `json:"event_id,omitempty"`
|
|
Extra map[string]interface{} `json:"extra,omitempty"`
|
|
Fingerprint []string `json:"fingerprint,omitempty"`
|
|
Level Level `json:"level,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Platform string `json:"platform,omitempty"`
|
|
Release string `json:"release,omitempty"`
|
|
Sdk SdkInfo `json:"sdk,omitempty"`
|
|
ServerName string `json:"server_name,omitempty"`
|
|
Threads []Thread `json:"threads,omitempty"`
|
|
Tags map[string]string `json:"tags,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Transaction string `json:"transaction,omitempty"`
|
|
User User `json:"user,omitempty"`
|
|
Logger string `json:"logger,omitempty"`
|
|
Modules map[string]string `json:"modules,omitempty"`
|
|
Request *Request `json:"request,omitempty"`
|
|
Exception []Exception `json:"exception,omitempty"`
|
|
|
|
// Experimental: This is part of a beta feature of the SDK. The fields below
|
|
// are only relevant for transactions.
|
|
Type string `json:"type,omitempty"`
|
|
StartTimestamp time.Time `json:"start_timestamp"`
|
|
Spans []*Span `json:"spans,omitempty"`
|
|
}
|
|
|
|
// MarshalJSON converts the Event struct to JSON.
|
|
func (e *Event) MarshalJSON() ([]byte, error) {
|
|
// event aliases Event to allow calling json.Marshal without an infinite
|
|
// loop. It preserves all fields of Event while none of the attached
|
|
// methods.
|
|
type event Event
|
|
|
|
// Transactions are marshaled in the standard way how json.Marshal works.
|
|
if e.Type == transactionType {
|
|
return json.Marshal((*event)(e))
|
|
}
|
|
|
|
// errorEvent is like Event with some shadowed fields for customizing the
|
|
// JSON serialization of regular "error events".
|
|
type errorEvent struct {
|
|
*event
|
|
|
|
// encoding/json doesn't support the omitempty option for struct types.
|
|
// See https://golang.org/issues/11939.
|
|
// We shadow the original Event.Timestamp field with a json.RawMessage.
|
|
// This allows us to include the timestamp when non-zero and omit it
|
|
// otherwise.
|
|
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
|
|
|
// The fields below are not part of the regular "error events" and only
|
|
// make sense to be sent for transactions. They shadow the respective
|
|
// fields in Event and are meant to remain nil, triggering the omitempty
|
|
// behavior.
|
|
Type json.RawMessage `json:"type,omitempty"`
|
|
StartTimestamp json.RawMessage `json:"start_timestamp,omitempty"`
|
|
Spans json.RawMessage `json:"spans,omitempty"`
|
|
}
|
|
|
|
x := &errorEvent{event: (*event)(e)}
|
|
if !e.Timestamp.IsZero() {
|
|
x.Timestamp = append(x.Timestamp, '"')
|
|
x.Timestamp = e.Timestamp.UTC().AppendFormat(x.Timestamp, time.RFC3339Nano)
|
|
x.Timestamp = append(x.Timestamp, '"')
|
|
}
|
|
return json.Marshal(x)
|
|
}
|
|
|
|
// NewEvent creates a new Event.
|
|
func NewEvent() *Event {
|
|
event := Event{
|
|
Contexts: make(map[string]interface{}),
|
|
Extra: make(map[string]interface{}),
|
|
Tags: make(map[string]string),
|
|
Modules: make(map[string]string),
|
|
}
|
|
return &event
|
|
}
|
|
|
|
// Thread specifies threads that were running at the time of an event.
|
|
type Thread struct {
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
|
RawStacktrace *Stacktrace `json:"raw_stacktrace,omitempty"`
|
|
Crashed bool `json:"crashed,omitempty"`
|
|
Current bool `json:"current,omitempty"`
|
|
}
|
|
|
|
// EventHint contains information that can be associated with an Event.
|
|
type EventHint struct {
|
|
Data interface{}
|
|
EventID string
|
|
OriginalException error
|
|
RecoveredException interface{}
|
|
Context context.Context
|
|
Request *http.Request
|
|
Response *http.Response
|
|
}
|
|
|
|
// TraceContext describes the context of the trace.
|
|
//
|
|
// Experimental: This is part of a beta feature of the SDK.
|
|
type TraceContext struct {
|
|
TraceID string `json:"trace_id"`
|
|
SpanID string `json:"span_id"`
|
|
Op string `json:"op,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
// Span describes a timed unit of work in a trace.
|
|
//
|
|
// Experimental: This is part of a beta feature of the SDK.
|
|
type Span struct {
|
|
TraceID string `json:"trace_id"`
|
|
SpanID string `json:"span_id"`
|
|
ParentSpanID string `json:"parent_span_id,omitempty"`
|
|
Op string `json:"op,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Tags map[string]string `json:"tags,omitempty"`
|
|
StartTimestamp time.Time `json:"start_timestamp"`
|
|
EndTimestamp time.Time `json:"timestamp"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
}
|