285 lines
6.8 KiB
Go
285 lines
6.8 KiB
Go
package irontelemetry
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Client is the main IronTelemetry client
|
|
type Client struct {
|
|
mu sync.RWMutex
|
|
opts Options
|
|
parsedDSN *ParsedDSN
|
|
transport *Transport
|
|
breadcrumbs *BreadcrumbManager
|
|
journeys *JourneyManager
|
|
user *User
|
|
tags map[string]string
|
|
extra map[string]any
|
|
initialized bool
|
|
}
|
|
|
|
// New creates a new IronTelemetry client
|
|
func New(opts Options) (*Client, error) {
|
|
resolvedOpts, parsedDSN, err := ResolveOptions(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := &Client{
|
|
opts: resolvedOpts,
|
|
parsedDSN: parsedDSN,
|
|
transport: NewTransport(parsedDSN, resolvedOpts.APIBaseURL, resolvedOpts.Debug),
|
|
breadcrumbs: NewBreadcrumbManager(resolvedOpts.MaxBreadcrumbs),
|
|
tags: make(map[string]string),
|
|
extra: make(map[string]any),
|
|
initialized: true,
|
|
}
|
|
client.journeys = NewJourneyManager(client)
|
|
|
|
if opts.Debug {
|
|
fmt.Printf("[IronTelemetry] Initialized with DSN: %s\n", parsedDSN.APIBaseURL)
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// CaptureException captures an error and sends it to the server
|
|
func (c *Client) CaptureException(err error) SendResult {
|
|
return c.CaptureExceptionWithContext(context.Background(), err)
|
|
}
|
|
|
|
// CaptureExceptionWithContext captures an error with context
|
|
func (c *Client) CaptureExceptionWithContext(ctx context.Context, err error) SendResult {
|
|
if err == nil {
|
|
return SendResult{Success: false, Error: "nil error"}
|
|
}
|
|
|
|
event := c.createEvent(SeverityError, err.Error())
|
|
event.Exception = &ExceptionInfo{
|
|
Type: fmt.Sprintf("%T", err),
|
|
Message: err.Error(),
|
|
Stacktrace: captureStacktrace(3),
|
|
}
|
|
|
|
return c.sendEvent(ctx, event)
|
|
}
|
|
|
|
// CaptureMessage captures a message and sends it to the server
|
|
func (c *Client) CaptureMessage(message string, level SeverityLevel) SendResult {
|
|
return c.CaptureMessageWithContext(context.Background(), message, level)
|
|
}
|
|
|
|
// CaptureMessageWithContext captures a message with context
|
|
func (c *Client) CaptureMessageWithContext(ctx context.Context, message string, level SeverityLevel) SendResult {
|
|
event := c.createEvent(level, message)
|
|
return c.sendEvent(ctx, event)
|
|
}
|
|
|
|
// AddBreadcrumb adds a breadcrumb
|
|
func (c *Client) AddBreadcrumb(message string, category BreadcrumbCategory) {
|
|
c.breadcrumbs.AddSimple(message, category)
|
|
}
|
|
|
|
// AddBreadcrumbWithLevel adds a breadcrumb with a level
|
|
func (c *Client) AddBreadcrumbWithLevel(message string, category BreadcrumbCategory, level SeverityLevel) {
|
|
c.breadcrumbs.AddWithLevel(message, category, level)
|
|
}
|
|
|
|
// AddBreadcrumbWithData adds a breadcrumb with data
|
|
func (c *Client) AddBreadcrumbWithData(message string, category BreadcrumbCategory, level SeverityLevel, data map[string]any) {
|
|
c.breadcrumbs.AddWithData(message, category, level, data)
|
|
}
|
|
|
|
// SetUser sets the user context
|
|
func (c *Client) SetUser(user *User) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.user = user
|
|
}
|
|
|
|
// SetUserByID sets the user context by ID
|
|
func (c *Client) SetUserByID(id string) {
|
|
c.SetUser(&User{ID: id})
|
|
}
|
|
|
|
// SetUserWithEmail sets the user context with ID and email
|
|
func (c *Client) SetUserWithEmail(id, email string) {
|
|
c.SetUser(&User{ID: id, Email: email})
|
|
}
|
|
|
|
// SetTag sets a tag
|
|
func (c *Client) SetTag(key, value string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.tags[key] = value
|
|
}
|
|
|
|
// SetTags sets multiple tags
|
|
func (c *Client) SetTags(tags map[string]string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for k, v := range tags {
|
|
c.tags[k] = v
|
|
}
|
|
}
|
|
|
|
// SetExtra sets extra data
|
|
func (c *Client) SetExtra(key string, value any) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.extra[key] = value
|
|
}
|
|
|
|
// SetExtras sets multiple extra data values
|
|
func (c *Client) SetExtras(extras map[string]any) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for k, v := range extras {
|
|
c.extra[k] = v
|
|
}
|
|
}
|
|
|
|
// StartJourney starts a new journey
|
|
func (c *Client) StartJourney(name string) *Journey {
|
|
return c.journeys.StartJourney(name)
|
|
}
|
|
|
|
// GetCurrentJourney returns the current journey context
|
|
func (c *Client) GetCurrentJourney() *JourneyContext {
|
|
return c.journeys.GetCurrent()
|
|
}
|
|
|
|
// ClearBreadcrumbs clears all breadcrumbs
|
|
func (c *Client) ClearBreadcrumbs() {
|
|
c.breadcrumbs.Clear()
|
|
}
|
|
|
|
// Flush flushes any pending events (placeholder for offline queue)
|
|
func (c *Client) Flush() {
|
|
// Future: Implement offline queue flushing
|
|
}
|
|
|
|
// Close closes the client and flushes pending events
|
|
func (c *Client) Close() {
|
|
c.Flush()
|
|
}
|
|
|
|
func (c *Client) createEvent(level SeverityLevel, message string) *TelemetryEvent {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
// Copy tags
|
|
tags := make(map[string]string, len(c.tags))
|
|
for k, v := range c.tags {
|
|
tags[k] = v
|
|
}
|
|
|
|
// Copy extra
|
|
extra := make(map[string]any, len(c.extra))
|
|
for k, v := range c.extra {
|
|
extra[k] = v
|
|
}
|
|
|
|
event := &TelemetryEvent{
|
|
EventID: GenerateEventID(),
|
|
Timestamp: time.Now(),
|
|
Level: level,
|
|
Message: message,
|
|
Tags: tags,
|
|
Extra: extra,
|
|
Breadcrumbs: c.breadcrumbs.GetAll(),
|
|
Environment: c.opts.Environment,
|
|
AppVersion: c.opts.AppVersion,
|
|
Platform: PlatformInfo{
|
|
Name: "go",
|
|
Version: runtime.Version(),
|
|
OS: runtime.GOOS + "/" + runtime.GOARCH,
|
|
},
|
|
}
|
|
|
|
// Copy user if set
|
|
if c.user != nil {
|
|
event.User = &User{
|
|
ID: c.user.ID,
|
|
Email: c.user.Email,
|
|
Name: c.user.Name,
|
|
}
|
|
if c.user.Data != nil {
|
|
event.User.Data = make(map[string]any, len(c.user.Data))
|
|
for k, v := range c.user.Data {
|
|
event.User.Data[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy journey if active
|
|
if journey := c.journeys.GetCurrent(); journey != nil {
|
|
event.Journey = &JourneyContext{
|
|
JourneyID: journey.JourneyID,
|
|
Name: journey.Name,
|
|
CurrentStep: journey.CurrentStep,
|
|
StartedAt: journey.StartedAt,
|
|
Metadata: make(map[string]any, len(journey.Metadata)),
|
|
}
|
|
for k, v := range journey.Metadata {
|
|
event.Journey.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
func (c *Client) sendEvent(ctx context.Context, event *TelemetryEvent) SendResult {
|
|
// Check sample rate
|
|
if c.opts.SampleRate < 1.0 && rand.Float64() > c.opts.SampleRate {
|
|
if c.opts.Debug {
|
|
fmt.Printf("[IronTelemetry] Event sampled out: %s\n", event.EventID)
|
|
}
|
|
return SendResult{Success: true, EventID: event.EventID}
|
|
}
|
|
|
|
// Apply beforeSend hook
|
|
if c.opts.BeforeSend != nil {
|
|
event = c.opts.BeforeSend(event)
|
|
if event == nil {
|
|
if c.opts.Debug {
|
|
fmt.Println("[IronTelemetry] Event dropped by beforeSend hook")
|
|
}
|
|
return SendResult{Success: true}
|
|
}
|
|
}
|
|
|
|
return c.transport.Send(ctx, event)
|
|
}
|
|
|
|
func captureStacktrace(skip int) []StackFrame {
|
|
var frames []StackFrame
|
|
pcs := make([]uintptr, 32)
|
|
n := runtime.Callers(skip, pcs)
|
|
if n == 0 {
|
|
return frames
|
|
}
|
|
|
|
pcs = pcs[:n]
|
|
runtimeFrames := runtime.CallersFrames(pcs)
|
|
|
|
for {
|
|
frame, more := runtimeFrames.Next()
|
|
frames = append(frames, StackFrame{
|
|
Function: frame.Function,
|
|
Filename: frame.File,
|
|
Lineno: frame.Line,
|
|
})
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
|
|
return frames
|
|
}
|