irontelemetry-go/transport.go

172 lines
4.1 KiB
Go

package irontelemetry
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Transport handles HTTP communication with the server
type Transport struct {
apiBaseURL string
publicKey string
debug bool
client *http.Client
}
// NewTransport creates a new Transport
func NewTransport(parsedDSN *ParsedDSN, apiBaseURL string, debug bool) *Transport {
return &Transport{
apiBaseURL: apiBaseURL,
publicKey: parsedDSN.PublicKey,
debug: debug,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// Send sends an event to the server
func (t *Transport) Send(ctx context.Context, event *TelemetryEvent) SendResult {
url := t.apiBaseURL + "/api/v1/events"
body, err := json.Marshal(t.serializeEvent(event))
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Public-Key", t.publicKey)
resp, err := t.client.Do(req)
if err != nil {
if t.debug {
fmt.Printf("[IronTelemetry] Failed to send event: %v\n", err)
}
return SendResult{Success: false, Error: err.Error()}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
bodyBytes, _ := io.ReadAll(resp.Body)
errMsg := fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
if t.debug {
fmt.Printf("[IronTelemetry] Failed to send event: %s\n", errMsg)
}
return SendResult{Success: false, Error: errMsg}
}
var result struct {
EventID string `json:"eventId"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// Even if we can't decode, the request succeeded
result.EventID = event.EventID
}
if t.debug {
fmt.Printf("[IronTelemetry] Event sent successfully: %s\n", event.EventID)
}
return SendResult{
Success: true,
EventID: result.EventID,
}
}
// IsOnline checks if the server is reachable
func (t *Transport) IsOnline(ctx context.Context) bool {
url := t.apiBaseURL + "/api/v1/health"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return false
}
req.Header.Set("X-Public-Key", t.publicKey)
resp, err := t.client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
func (t *Transport) serializeEvent(event *TelemetryEvent) map[string]any {
breadcrumbs := make([]map[string]any, len(event.Breadcrumbs))
for i, b := range event.Breadcrumbs {
breadcrumbs[i] = map[string]any{
"timestamp": b.Timestamp.Format(time.RFC3339),
"category": b.Category,
"message": b.Message,
"level": b.Level,
"data": b.Data,
}
}
result := map[string]any{
"eventId": event.EventID,
"timestamp": event.Timestamp.Format(time.RFC3339),
"level": event.Level,
"message": event.Message,
"tags": event.Tags,
"extra": event.Extra,
"breadcrumbs": breadcrumbs,
"environment": event.Environment,
"appVersion": event.AppVersion,
"platform": map[string]any{
"name": event.Platform.Name,
"version": event.Platform.Version,
"os": event.Platform.OS,
},
}
if event.Exception != nil {
stacktrace := make([]map[string]any, len(event.Exception.Stacktrace))
for i, f := range event.Exception.Stacktrace {
stacktrace[i] = map[string]any{
"function": f.Function,
"filename": f.Filename,
"lineno": f.Lineno,
}
}
result["exception"] = map[string]any{
"type": event.Exception.Type,
"message": event.Exception.Message,
"stacktrace": stacktrace,
}
}
if event.User != nil {
result["user"] = map[string]any{
"id": event.User.ID,
"email": event.User.Email,
"name": event.User.Name,
"data": event.User.Data,
}
}
if event.Journey != nil {
result["journey"] = map[string]any{
"journeyId": event.Journey.JourneyID,
"name": event.Journey.Name,
"currentStep": event.Journey.CurrentStep,
"startedAt": event.Journey.StartedAt.Format(time.RFC3339),
"metadata": event.Journey.Metadata,
}
}
return result
}