Skip to content

Commit

Permalink
feat: Ingest Android profile chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
phacops committed Oct 17, 2024
1 parent 8568b2b commit 26a0d09
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 259 deletions.
86 changes: 57 additions & 29 deletions cmd/vroom/chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"github.com/getsentry/vroom/internal/storageutil"
)

type (
chunkPlatform struct {
Platform platform.Platform `json:"platform"`
}
)

func (env *environment) postChunk(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hub := sentry.GetHubFromContext(ctx)
Expand All @@ -36,9 +42,26 @@ func (env *environment) postChunk(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
r.Body.Close()

var p chunkPlatform
err = json.Unmarshal(body, &p)
if err != nil {
if hub != nil {
hub.CaptureException(err)
}
w.WriteHeader(http.StatusBadRequest)
return
}

var c chunk.Chunk
switch p.Platform {
case platform.Android:
c = new(chunk.AndroidChunk)
default:
c = new(chunk.SampleChunk)
}

c := new(chunk.Chunk)
s = sentry.StartSpan(ctx, "json.unmarshal")
s.Description = "Unmarshal profile"
err = json.Unmarshal(body, c)
Expand All @@ -51,19 +74,21 @@ func (env *environment) postChunk(w http.ResponseWriter, r *http.Request) {
return
}

c.Normalize()
if p.Platform != platform.Android {
c.(*chunk.SampleChunk).Normalize()
}

if hub != nil {
hub.Scope().SetContext("Profile metadata", map[string]interface{}{
"chunk_id": c.ID,
"organization_id": strconv.FormatUint(c.OrganizationID, 10),
"profiler_id": c.ProfilerID,
"project_id": strconv.FormatUint(c.ProjectID, 10),
"chunk_id": c.GetID(),
"organization_id": strconv.FormatUint(c.GetOrganizationID(), 10),
"profiler_id": c.GetProfilerID(),
"project_id": strconv.FormatUint(c.GetProjectID(), 10),
"size": len(body),
})

hub.Scope().SetTags(map[string]string{
"platform": string(c.Platform),
"platform": string(c.GetPlatform()),
})
}

Expand Down Expand Up @@ -115,14 +140,17 @@ func (env *environment) postChunk(w http.ResponseWriter, r *http.Request) {
}
s.Finish()

if c.Options.ProjectDSN != "" {
options := c.GetOptions()
sc, ok := c.(chunk.SampleChunk)

if options.ProjectDSN != "" && c.GetPlatform() != platform.Android && ok {
// nb.: here we don't have a specific thread ID, so we're going to ingest
// functions metrics from all the thread.
// That's ok as this data is not supposed to be transaction/span scoped,
// plus, we'll only retain application frames, so much of the system functions
// chaff will be dropped.
s = sentry.StartSpan(ctx, "processing")
callTrees, err := c.CallTrees(nil)
callTrees, err := sc.CallTrees(nil)
s.Finish()
if err != nil {
hub.CaptureException(err)
Expand All @@ -138,13 +166,13 @@ func (env *environment) postChunk(w http.ResponseWriter, r *http.Request) {

s = sentry.StartSpan(ctx, "processing")
s.Description = "Extract metrics from functions"
metrics := extractMetricsFromChunkFunctions(c, functions)
metrics := extractMetricsFromSampleChunkFunctions(&sc, functions)
s.Finish()

if len(metrics) > 0 {
s = sentry.StartSpan(ctx, "processing")
s.Description = "Send functions metrics to generic metrics platform"
sendMetrics(ctx, c.Options.ProjectDSN, metrics, env.metricsClient)
sendMetrics(ctx, options.ProjectDSN, metrics, env.metricsClient)
s.Finish()
}
}
Expand All @@ -164,7 +192,7 @@ type postProfileFromChunkIDsRequest struct {
// This way, if we decide to later add a few more utility fields
// (for pagination, etc.) we won't have to change the Chunk struct.
type postProfileFromChunkIDsResponse struct {
Chunk chunk.Chunk `json:"chunk"`
Chunk chunk.SampleChunk `json:"chunk"`
}

// This is more of a GET method, but since we're receiving a list of chunk IDs as part of a
Expand Down Expand Up @@ -202,6 +230,7 @@ func (env *environment) postProfileFromChunkIDs(w http.ResponseWriter, r *http.R
w.WriteHeader(http.StatusBadRequest)
return
}
r.Body.Close()

hub.Scope().SetTag("num_chunks", fmt.Sprintf("%d", len(requestBody.ChunkIDs)))
s = sentry.StartSpan(ctx, "chunks.read")
Expand All @@ -222,7 +251,7 @@ func (env *environment) postProfileFromChunkIDs(w http.ResponseWriter, r *http.R
}
}

chunks := make([]chunk.Chunk, 0, len(requestBody.ChunkIDs))
chunks := make([]chunk.SampleChunk, 0, len(requestBody.ChunkIDs))
// read the output of each tasks
for i := 0; i < len(requestBody.ChunkIDs); i++ {
res := <-results
Expand Down Expand Up @@ -266,7 +295,7 @@ func (env *environment) postProfileFromChunkIDs(w http.ResponseWriter, r *http.R

s = sentry.StartSpan(ctx, "chunks.merge")
s.Description = "Merge profile chunks into a single one"
chunk, err := chunk.MergeChunks(chunks, requestBody.Start, requestBody.End)
chunk, err := chunk.MergeSampleChunks(chunks, requestBody.Start, requestBody.End)
s.Finish()
if err != nil {
hub.CaptureException(err)
Expand Down Expand Up @@ -309,21 +338,20 @@ type (
}
)

func buildChunkKafkaMessage(c *chunk.Chunk) *ChunkKafkaMessage {
start, end := c.StartEndTimestamps()
func buildChunkKafkaMessage(c chunk.Chunk) *ChunkKafkaMessage {
return &ChunkKafkaMessage{
ChunkID: c.ID,
ChunkID: c.GetID(),
DurationMS: c.DurationMS(),
EndTimestamp: end,
Environment: c.Environment,
Platform: c.Platform,
ProfilerID: c.ProfilerID,
ProjectID: c.ProjectID,
Received: c.Received,
Release: c.Release,
RetentionDays: c.RetentionDays,
SDKName: c.ClientSDK.Name,
SDKVersion: c.ClientSDK.Version,
StartTimestamp: start,
EndTimestamp: c.EndTimestamp(),
Environment: c.GetEnvironment(),
Platform: c.GetPlatform(),
ProfilerID: c.GetProfilerID(),
ProjectID: c.GetProjectID(),
Received: c.GetReceived(),
Release: c.GetRelease(),
RetentionDays: c.GetRetentionDays(),
SDKName: c.SDKName(),
SDKVersion: c.SDKVersion(),
StartTimestamp: c.StartTimestamp(),
}
}
9 changes: 5 additions & 4 deletions cmd/vroom/chunk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/getsentry/vroom/internal/testutil"
"github.com/google/uuid"
"github.com/segmentio/kafka-go"

"gocloud.dev/blob"
_ "gocloud.dev/blob/fileblob"
)
Expand Down Expand Up @@ -48,18 +49,18 @@ func TestMain(m *testing.M) {
os.Exit(code)
}

func TestPostAndReadChunk(t *testing.T) {
func TestPostAndReadSampleChunk(t *testing.T) {
profilerID := uuid.New().String()
chunkID := uuid.New().String()
chunkData := chunk.Chunk{
chunkData := chunk.SampleChunk{
ID: chunkID,
ProfilerID: profilerID,
Environment: "dev",
Platform: "python",
Release: "1.2",
OrganizationID: 1,
ProjectID: 1,
Profile: chunk.Data{
Profile: chunk.SampleData{
Frames: []frame.Frame{
{
Function: "test",
Expand Down Expand Up @@ -124,7 +125,7 @@ func TestPostAndReadChunk(t *testing.T) {

// read the chunk with UnmarshalCompressed and make sure that we can unmarshal
// the data into the Chunk struct and that it matches the original
var c chunk.Chunk
var c chunk.SampleChunk
err = storageutil.UnmarshalCompressed(
context.Background(),
test.blobBucket,
Expand Down
2 changes: 1 addition & 1 deletion cmd/vroom/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ type MetricSummary struct {
Count uint64
}

func extractMetricsFromChunkFunctions(c *chunk.Chunk, functions []nodetree.CallTreeFunction) []sentry.Metric {
func extractMetricsFromSampleChunkFunctions(c *chunk.SampleChunk, functions []nodetree.CallTreeFunction) []sentry.Metric {
metrics := make([]sentry.Metric, 0, len(functions))

for _, function := range functions {
Expand Down
113 changes: 113 additions & 0 deletions internal/chunk/android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package chunk

import (
"encoding/json"
"time"

"github.com/getsentry/vroom/internal/clientsdk"
"github.com/getsentry/vroom/internal/debugmeta"
"github.com/getsentry/vroom/internal/nodetree"
"github.com/getsentry/vroom/internal/platform"
"github.com/getsentry/vroom/internal/profile"
"github.com/getsentry/vroom/internal/utils"
)

type (
AndroidChunk struct {
BuildID string `json:"build_id,omitempty"`
ID string `json:"chunk_id"`
ProfilerID string `json:"profiler_id"`

DebugMeta debugmeta.DebugMeta `json:"debug_meta"`

ClientSDK clientsdk.ClientSDK `json:"client_sdk"`
DurationNS uint64 `json:"duration_ns"`
Environment string `json:"environment"`
Platform platform.Platform `json:"platform"`
Release string `json:"release"`
Timestamp float64 `json:"timestamp"`

Profile profile.Android `json:"profile"`
Measurements json.RawMessage `json:"measurements"`

OrganizationID uint64 `json:"organization_id"`
ProjectID uint64 `json:"project_id"`
Received float64 `json:"received"`
RetentionDays int `json:"retention_days"`

Options utils.Options `json:"options,omitempty"`
}
)

func (c AndroidChunk) StoragePath() string {
return StoragePath(
c.OrganizationID,
c.ProjectID,
c.ProfilerID,
c.ID,
)
}

func (c AndroidChunk) DurationMS() uint64 {
return uint64(time.Duration(c.DurationNS).Milliseconds())
}

func (c AndroidChunk) CallTrees() map[uint64][]*nodetree.Node {
return c.Profile.CallTrees()
}

func (c AndroidChunk) SDKName() string {
return c.ClientSDK.Name
}

func (c AndroidChunk) SDKVersion() string {
return c.ClientSDK.Version
}

func (c AndroidChunk) EndTimestamp() float64 {
return 0
}

func (c AndroidChunk) GetEnvironment() string {
return c.Environment
}

func (c AndroidChunk) GetID() string {
return c.ID
}

func (c AndroidChunk) GetPlatform() platform.Platform {
return c.Platform
}

func (c AndroidChunk) GetProfilerID() string {
return c.ProfilerID
}

func (c AndroidChunk) GetProjectID() uint64 {
return c.ProjectID
}

func (c AndroidChunk) GetReceived() float64 {
return c.Received
}

func (c AndroidChunk) GetRelease() string {
return c.Release
}

func (c AndroidChunk) GetRetentionDays() int {
return c.RetentionDays
}

func (c AndroidChunk) StartTimestamp() float64 {
return 0
}

func (c AndroidChunk) GetOrganizationID() uint64 {
return c.OrganizationID
}

func (c AndroidChunk) GetOptions() utils.Options {
return c.Options
}
Loading

0 comments on commit 26a0d09

Please sign in to comment.