irontelemetry-go/client.go

321 lines
8.1 KiB
Go

package irontelemetry
import (
"context"
"fmt"
"math/rand"
"runtime"
"sync"
"time"
)
// EnableDebugLogging is a global flag that enables debug logging for all clients.
// When enabled, all IronTelemetry clients will output debug information to stdout.
var EnableDebugLogging = false
// 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 || EnableDebugLogging {
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)
}
// LogMessage logs a structured message with title, message, and optional data.
// Useful for structured logging that differentiates the log title from its details.
func (c *Client) LogMessage(level SeverityLevel, title string, message string, data map[string]any) SendResult {
return c.LogMessageWithContext(context.Background(), level, title, message, data)
}
// LogMessageWithContext logs a structured message with context
func (c *Client) LogMessageWithContext(ctx context.Context, level SeverityLevel, title, message string, data map[string]any) SendResult {
var fullMessage string
if message != "" {
fullMessage = fmt.Sprintf("%s: %s", title, message)
} else {
fullMessage = title
}
event := c.createEvent(level, fullMessage)
c.mu.Lock()
event.Extra["logTitle"] = title
if data != nil {
event.Extra["logData"] = data
}
c.mu.Unlock()
return c.sendEvent(ctx, event)
}
// GetBreadcrumbs returns a copy of the current breadcrumbs list
func (c *Client) GetBreadcrumbs() []Breadcrumb {
return c.breadcrumbs.GetAll()
}
// 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 || EnableDebugLogging {
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 || EnableDebugLogging {
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
}