Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ php/vardumper/box.phar
php/vardumper/micro-*.sfx
php/vardumper/vardumper-parser.phar
modules/vardumper/bin/vardumper-parser-*
/internal/frontend/dist/*
/internal/frontend/dist/*
fix.patch
.claude/settings.local.json
3 changes: 2 additions & 1 deletion internal/server/http/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ func detectEventType(r *http.Request) *DetectedEvent {
}

// Method 3: SDK-specific headers that identify the event type.
if r.Header.Get("X-Sentry-Auth") != "" || strings.HasSuffix(r.URL.Path, "/envelope") || strings.HasSuffix(r.URL.Path, "/store") {
isSentryStore := strings.HasSuffix(r.URL.Path, "/store") && !strings.Contains(r.URL.Path, "/profiler/")
if r.Header.Get("X-Sentry-Auth") != "" || strings.HasSuffix(r.URL.Path, "/envelope") || isSentryStore {
return &DetectedEvent{Type: "sentry"}
}
if r.Header.Get("X-Inspector-Key") != "" || r.Header.Get("X-Inspector-Version") != "" {
Expand Down
7 changes: 7 additions & 0 deletions internal/server/http/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ func TestDetectEventType(t *testing.T) {
},
wantType: "sentry",
},
{
name: "profiler store path does not detect sentry",
makeRequest: func() *nethttp.Request {
return httptest.NewRequest("POST", "http://localhost/api/profiler/store", nil)
},
wantNil: true,
},
{
name: "X-Inspector-Key header detects inspector",
makeRequest: func() *nethttp.Request {
Expand Down
15 changes: 10 additions & 5 deletions modules/profiler/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func handleFlameChart(db *sql.DB) http.HandlerFunc {
}
}

visited := make(map[string]bool)
var buildNode func(e EdgeRow, start float64) *flameNode
buildNode = func(e EdgeRow, start float64) *flameNode {
val := float64(GetEdgeMetricValue(&e, metric))
Expand All @@ -285,11 +286,15 @@ func handleFlameChart(db *sql.DB) http.HandlerFunc {
Color: flameColor(pct),
}

childStart := start
for _, child := range childrenMap[e.Callee] {
childNode := buildNode(child, childStart)
node.Children = append(node.Children, childNode)
childStart += childNode.Duration
if !visited[e.Callee] {
visited[e.Callee] = true
childStart := start
for _, child := range childrenMap[e.Callee] {
childNode := buildNode(child, childStart)
node.Children = append(node.Children, childNode)
childStart += childNode.Duration
}
visited[e.Callee] = false
}
if node.Children == nil {
node.Children = []*flameNode{}
Expand Down
52 changes: 3 additions & 49 deletions modules/profiler/handler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package profiler

import (
"database/sql"
"encoding/json"
"io"
"net/http"
Expand All @@ -11,7 +10,6 @@ import (
)

type handler struct {
db *sql.DB
}

func (h *handler) Priority() int { return 40 }
Expand Down Expand Up @@ -40,21 +38,19 @@ func (h *handler) Handle(r *http.Request) (*event.Incoming, error) {
// Process the profile (compute diffs, percentages, edges).
peaks, edges := Process(&incoming)

// Store in profiler-specific tables.
uuid := event.GenerateUUID()
if err := storeProfile(h.db, uuid, incoming.AppName, peaks, edges); err != nil {
return nil, err
}

// Build event payload matching PHP Buggregator format.
// profile_uuid is used by the frontend to fetch call-graph/top/flame-chart.
// peaks and edges are embedded so OnEventStored can store them
// without re-parsing the original payload.
payload := map[string]any{
"profile_uuid": uuid,
"app_name": incoming.AppName,
"hostname": incoming.Hostname,
"date": incoming.Date,
"tags": incoming.Tags,
"peaks": peaks,
"edges": edges,
"total_edges": len(edges),
}
b, _ := json.Marshal(payload)
Expand All @@ -65,45 +61,3 @@ func (h *handler) Handle(r *http.Request) (*event.Incoming, error) {
Payload: json.RawMessage(b),
}, nil
}

func storeProfile(db *sql.DB, uuid, name string, peaks Metrics, edges map[string]Edge) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

_, err = tx.Exec(
`INSERT INTO profiles (uuid, name, cpu, wt, ct, mu, pmu) VALUES (?, ?, ?, ?, ?, ?, ?)`,
uuid, name, peaks.CPU, peaks.WallTime, peaks.Calls, peaks.Memory, peaks.PeakMem,
)
if err != nil {
return err
}

stmt, err := tx.Prepare(`INSERT INTO profile_edges
(uuid, profile_uuid, "order", cpu, wt, ct, mu, pmu, d_cpu, d_wt, d_ct, d_mu, d_pmu, p_cpu, p_wt, p_ct, p_mu, p_pmu, callee, caller, parent_uuid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
defer stmt.Close()

order := 0
for _, edge := range edges {
edgeUUID := event.GenerateUUID()
_, err = stmt.Exec(
edgeUUID, uuid, order,
edge.Cost.CPU, edge.Cost.WallTime, edge.Cost.Calls, edge.Cost.Memory, edge.Cost.PeakMem,
edge.Diff.CPU, edge.Diff.WallTime, edge.Diff.Calls, edge.Diff.Memory, edge.Diff.PeakMem,
edge.Percents.CPU, edge.Percents.WallTime, edge.Percents.Calls, edge.Percents.Memory, edge.Percents.PeakMem,
edge.Callee, edge.Caller, edge.Parent,
)
if err != nil {
return err
}
order++
}

return tx.Commit()
}
25 changes: 24 additions & 1 deletion modules/profiler/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package profiler

import (
"database/sql"
"encoding/json"
"log/slog"
"net/http"

"github.com/buggregator/go-buggregator/internal/event"
Expand Down Expand Up @@ -29,7 +31,7 @@ func (m *Module) OnInit(db *sql.DB) error {
}

func (m *Module) HTTPHandler() module.HTTPIngestionHandler {
return &handler{db: m.db}
return &handler{}
}

func (m *Module) RegisterRoutes(mux *http.ServeMux, store event.Store) {
Expand All @@ -39,3 +41,24 @@ func (m *Module) RegisterRoutes(mux *http.ServeMux, store event.Store) {
func (m *Module) PreviewMapper() event.PreviewMapper {
return &previewMapper{}
}

func (m *Module) OnEventStored(ev event.Event) {
if ev.Type != "profiler" || m.db == nil {
return
}

var payload struct {
ProfileUUID string `json:"profile_uuid"`
AppName string `json:"app_name"`
Peaks Metrics `json:"peaks"`
Edges map[string]Edge `json:"edges"`
}
if err := json.Unmarshal(ev.Payload, &payload); err != nil {
slog.Warn("profiler: failed to parse event payload", "err", err)
return
}

if err := storeProfile(m.db, payload.ProfileUUID, payload.AppName, payload.Peaks, payload.Edges); err != nil {
slog.Warn("profiler: failed to store profile", "err", err)
}
}
49 changes: 49 additions & 0 deletions modules/profiler/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package profiler

import (
"database/sql"

"github.com/buggregator/go-buggregator/internal/event"
)

func storeProfile(db *sql.DB, uuid, name string, peaks Metrics, edges map[string]Edge) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

_, err = tx.Exec(
`INSERT INTO profiles (uuid, name, cpu, wt, ct, mu, pmu) VALUES (?, ?, ?, ?, ?, ?, ?)`,
uuid, name, peaks.CPU, peaks.WallTime, peaks.Calls, peaks.Memory, peaks.PeakMem,
)
if err != nil {
return err
}

stmt, err := tx.Prepare(`INSERT INTO profile_edges
(uuid, profile_uuid, "order", cpu, wt, ct, mu, pmu, d_cpu, d_wt, d_ct, d_mu, d_pmu, p_cpu, p_wt, p_ct, p_mu, p_pmu, callee, caller, parent_uuid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
defer stmt.Close()

order := 0
for _, edge := range edges {
edgeUUID := event.GenerateUUID()
_, err = stmt.Exec(
edgeUUID, uuid, order,
edge.Cost.CPU, edge.Cost.WallTime, edge.Cost.Calls, edge.Cost.Memory, edge.Cost.PeakMem,
edge.Diff.CPU, edge.Diff.WallTime, edge.Diff.Calls, edge.Diff.Memory, edge.Diff.PeakMem,
edge.Percents.CPU, edge.Percents.WallTime, edge.Percents.Calls, edge.Percents.Memory, edge.Percents.PeakMem,
edge.Callee, edge.Caller, edge.Parent,
)
if err != nil {
return err
}
order++
}

return tx.Commit()
}
3 changes: 2 additions & 1 deletion modules/sentry/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func (h *handler) Match(r *http.Request) bool {
return true
}
path := r.URL.Path
return strings.HasSuffix(path, "/store") || strings.HasSuffix(path, "/envelope")
isSentryStore := strings.HasSuffix(path, "/store") && !strings.Contains(path, "/profiler/")
return isSentryStore || strings.HasSuffix(path, "/envelope")
}

func (h *handler) Handle(r *http.Request) (*event.Incoming, error) {
Expand Down
Loading