diff --git a/.trunk/configs/cspell.json b/.trunk/configs/cspell.json index 5849d78d3..9fa6ff39d 100644 --- a/.trunk/configs/cspell.json +++ b/.trunk/configs/cspell.json @@ -170,6 +170,7 @@ "santhosh", "schemagen", "sentryhttp", + "sentryutils", "sjson", "somedbtype", "sqlclient", diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8b163ec..1d406eb09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NOTE: all releases may include dependency updates, not specifically mentioned ## UNRELEASED - feat: integrate try-as library [#912](https://github.com/hypermodeinc/modus/pull/912) +- feat: improve sentry usage [#931](https://github.com/hypermodeinc/modus/pull/931) ## 2025-07-03 - Runtime v0.18.3 diff --git a/runtime/.goreleaser.yaml b/runtime/.goreleaser.yaml index 8a7d9203a..959aa8d03 100644 --- a/runtime/.goreleaser.yaml +++ b/runtime/.goreleaser.yaml @@ -24,8 +24,6 @@ builds: - arm64 mod_timestamp: "{{ .CommitTimestamp }}" ldflags: - - -s - - -w - -X github.com/hypermodeinc/modus/runtime/app.version={{.Version}} - >- {{- if eq .Os "windows"}} diff --git a/runtime/Makefile b/runtime/Makefile index 7179b2d1b..53cc0c6aa 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -1,6 +1,6 @@ EXECUTABLE := modus_runtime VERSION := $(shell git describe --tags --always --match 'runtime/*' | sed 's/^runtime\///') -LDFLAGS := -s -w -X github.com/hypermodeinc/modus/runtime/app.version=$(VERSION) +LDFLAGS := -X github.com/hypermodeinc/modus/runtime/app.version=$(VERSION) ifneq ($(OS), Windows_NT) OS := $(shell uname -s) diff --git a/runtime/actors/actorsystem.go b/runtime/actors/actorsystem.go index 98f26a2cc..f949404eb 100644 --- a/runtime/actors/actorsystem.go +++ b/runtime/actors/actorsystem.go @@ -20,6 +20,7 @@ import ( "github.com/hypermodeinc/modus/runtime/messages" "github.com/hypermodeinc/modus/runtime/pluginmanager" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modus/runtime/wasmhost" @@ -29,7 +30,7 @@ import ( var _actorSystem goakt.ActorSystem func Initialize(ctx context.Context) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() wasmExt := &wasmExtension{ @@ -48,15 +49,21 @@ func Initialize(ctx context.Context) { actorSystem, err := goakt.NewActorSystem("modus", opts...) if err != nil { - logger.Fatal(ctx, err).Msg("Failed to create actor system.") + const msg = "Failed to create actor system." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } if err := startActorSystem(ctx, actorSystem); err != nil { - logger.Fatal(ctx, err).Msg("Failed to start actor system.") + const msg = "Failed to start actor system." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } if err := actorSystem.Inject(&wasmAgentInfo{}); err != nil { - logger.Fatal(ctx, err).Msg("Failed to inject wasm agent info into actor system.") + const msg = "Failed to inject wasm agent info into actor system." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } _actorSystem = actorSystem @@ -88,7 +95,7 @@ func startActorSystem(ctx context.Context, actorSystem goakt.ActorSystem) error } func loadAgentActors(ctx context.Context, plugin *plugins.Plugin) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // restart local actors that are already running, which will reload the plugin @@ -96,7 +103,9 @@ func loadAgentActors(ctx context.Context, plugin *plugins.Plugin) error { for _, pid := range actors { if a, ok := pid.Actor().(*wasmAgentActor); ok { if err := goakt.Tell(ctx, pid, &messages.RestartAgent{}); err != nil { - logger.Error(ctx, err).Str("agent_id", a.agentId).Msg("Failed to send restart agent message to actor.") + const msg = "Failed to send restart agent message to actor." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", a.agentId)) + logger.Error(ctx, err).Str("agent_id", a.agentId).Msg(msg) } } } @@ -104,7 +113,9 @@ func loadAgentActors(ctx context.Context, plugin *plugins.Plugin) error { // do this in a goroutine to avoid blocking the cluster engine startup go func() { if err := restoreAgentActors(ctx, plugin.Name()); err != nil { - logger.Error(ctx, err).Msg("Failed to restore agent actors.") + const msg = "Failed to restore agent actors." + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) } }() @@ -113,7 +124,7 @@ func loadAgentActors(ctx context.Context, plugin *plugins.Plugin) error { // restoreAgentActors spawn actors for agents with state in the database, that are not already running func restoreAgentActors(ctx context.Context, pluginName string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Debug(ctx).Msg("Restoring agent actors from database.") @@ -133,11 +144,15 @@ func restoreAgentActors(ctx context.Context, pluginName string) error { for _, agent := range agents { actorName := getActorName(agent.Id) if exists, err := _actorSystem.ActorExists(ctx, actorName); err != nil { - logger.Error(ctx, err).Msgf("Failed to check if actor %s exists.", actorName) + const msg = "Failed to check if agent actor exists." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", agent.Id)) + logger.Error(ctx, err).Str("agent_id", agent.Id).Msg(msg) } else if !exists { err := spawnActorForAgent(ctx, pluginName, agent.Id, agent.Name, false) if err != nil { - logger.Error(ctx, err).Msgf("Failed to spawn actor for agent %s.", agent.Id) + const msg = "Failed to spawn actor for agent." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", agent.Id)) + logger.Error(ctx, err).Str("agent_id", agent.Id).Msg(msg) } } } @@ -146,7 +161,7 @@ func restoreAgentActors(ctx context.Context, pluginName string) error { } func beforeShutdown(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Info(ctx).Msg("Actor system shutting down...") @@ -159,29 +174,31 @@ func beforeShutdown(ctx context.Context) error { if actor.status == AgentStatusRunning { ctx := actor.augmentContext(ctx, pid) if err := actor.suspendAgent(ctx); err != nil { - logger.Error(ctx, err).Str("agent_id", actor.agentId).Msg("Failed to suspend agent actor.") + const msg = "Failed to suspend agent actor." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", actor.agentId)) + logger.Error(ctx, err).Str("agent_id", actor.agentId).Msg(msg) } } } } - // Then shut down subscription actors. They will have received the suspend message already. + // Then shut down subscription actors. They will have received the suspend message already. for _, pid := range actors { - if _, ok := pid.Actor().(*subscriptionActor); ok && pid.IsRunning() { + if a, ok := pid.Actor().(*subscriptionActor); ok && pid.IsRunning() { if err := pid.Shutdown(ctx); err != nil { - logger.Error(ctx, err).Msgf("Failed to shutdown actor %s.", pid.Name()) + const msg = "Failed to shut down subscription actor." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", a.agentId)) + logger.Error(ctx, err).Str("agent_id", a.agentId).Msg(msg) } } } - // waitForClusterSync() - - // then allow the actor system to continue with its shutdown process + // Then allow the actor system to continue with its shutdown process. return nil } // Waits for the peer sync interval to pass, allowing time for the actor system to synchronize its -// list of actors with the remote nodes in the cluster. Cancels early if the context is done. +// list of actors with the remote nodes in the cluster. Cancels early if the context is done. func waitForClusterSync(ctx context.Context) { if clusterEnabled() { select { @@ -193,15 +210,19 @@ func waitForClusterSync(ctx context.Context) { } func Shutdown(ctx context.Context) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if _actorSystem == nil { - logger.Fatal(ctx).Msg("Actor system is not initialized, cannot shutdown.") + const msg = "Actor system is not initialized, cannot shutdown." + sentryutils.CaptureError(ctx, nil, msg) + logger.Fatal(ctx).Msg(msg) } if err := _actorSystem.Stop(ctx); err != nil { - logger.Error(ctx, err).Msg("Failed to shutdown actor system.") + const msg = "Failed to shutdown actor system." + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) } logger.Info(ctx).Msg("Actor system shutdown complete.") diff --git a/runtime/actors/agents.go b/runtime/actors/agents.go index 33b908cab..6eb91a9ef 100644 --- a/runtime/actors/agents.go +++ b/runtime/actors/agents.go @@ -19,6 +19,7 @@ import ( "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/messages" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" goakt "github.com/tochemey/goakt/v3/actor" @@ -63,7 +64,7 @@ const ( ) func StartAgent(ctx context.Context, agentName string) (*AgentInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() plugin, ok := plugins.GetPluginFromContext(ctx) @@ -80,7 +81,7 @@ func StartAgent(ctx context.Context, agentName string) (*AgentInfo, error) { } func spawnActorForAgent(ctx context.Context, pluginName, agentId, agentName string, initializing bool) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() ctx = context.WithoutCancel(ctx) @@ -111,7 +112,7 @@ func spawnActorForAgent(ctx context.Context, pluginName, agentId, agentName stri } func StopAgent(ctx context.Context, agentId string) (*AgentInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() actorName := getActorName(agentId) @@ -137,7 +138,7 @@ func StopAgent(ctx context.Context, agentId string) (*AgentInfo, error) { } func getAgentInfoFromDatabase(ctx context.Context, agentId string) (*AgentInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if agent, e := db.GetAgentState(ctx, agentId); e == nil { @@ -151,7 +152,7 @@ func getAgentInfoFromDatabase(ctx context.Context, agentId string) (*AgentInfo, } func GetAgentInfo(ctx context.Context, agentId string) (*AgentInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() actorName := getActorName(agentId) @@ -199,7 +200,7 @@ func newAgentMessageErrorResponse(errMsg string) *agentMessageResponse { } func SendAgentMessage(ctx context.Context, agentId string, msgName string, data *string, timeout int64) (*agentMessageResponse, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() actorName := getActorName(agentId) @@ -234,7 +235,7 @@ func SendAgentMessage(ctx context.Context, agentId string, msgName string, data } func PublishAgentEvent(ctx context.Context, agentId, eventName string, eventData *string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() var data any @@ -304,7 +305,7 @@ func getAgentTopic(agentId string) string { } func ListActiveAgents(ctx context.Context) ([]AgentInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() agents, err := db.QueryActiveAgents(ctx) diff --git a/runtime/actors/cluster.go b/runtime/actors/cluster.go index ce4a58414..6fabee04c 100644 --- a/runtime/actors/cluster.go +++ b/runtime/actors/cluster.go @@ -20,6 +20,7 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" goakt "github.com/tochemey/goakt/v3/actor" @@ -31,7 +32,7 @@ import ( ) func clusterOptions(ctx context.Context) []goakt.Option { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() clusterMode := clusterMode() @@ -52,7 +53,9 @@ func clusterOptions(ctx context.Context) []goakt.Option { disco, err := newDiscoveryProvider(ctx, clusterMode, discoveryPort) if err != nil { - logger.Fatal(ctx, err).Msg("Failed to create cluster discovery provider.") + const msg = "Failed to create cluster discovery provider." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("cluster_mode", clusterMode.String())) + logger.Fatal(ctx, err).Msg(msg) } return []goakt.Option{ @@ -237,7 +240,7 @@ func getPodLabels() map[string]string { } func newDiscoveryProvider(ctx context.Context, clusterMode goaktClusterMode, discoveryPort int) (discovery.Provider, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() switch clusterMode { @@ -299,21 +302,21 @@ type providerWrapper struct { } func (w *providerWrapper) Close() error { - span, _ := utils.NewSentrySpanForCurrentFunc(w.ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(w.ctx) defer span.Finish() return w.provider.Close() } func (w *providerWrapper) Deregister() error { - span, _ := utils.NewSentrySpanForCurrentFunc(w.ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(w.ctx) defer span.Finish() return w.provider.Deregister() } func (w *providerWrapper) DiscoverPeers() ([]string, error) { - span, _ := utils.NewSentrySpanForCurrentFunc(w.ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(w.ctx) defer span.Finish() return w.provider.DiscoverPeers() @@ -324,14 +327,14 @@ func (w *providerWrapper) ID() string { } func (w *providerWrapper) Initialize() error { - span, _ := utils.NewSentrySpanForCurrentFunc(w.ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(w.ctx) defer span.Finish() return w.provider.Initialize() } func (w *providerWrapper) Register() error { - span, _ := utils.NewSentrySpanForCurrentFunc(w.ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(w.ctx) defer span.Finish() return w.provider.Register() diff --git a/runtime/actors/misc.go b/runtime/actors/misc.go index 2edd77818..d47747eb3 100644 --- a/runtime/actors/misc.go +++ b/runtime/actors/misc.go @@ -14,7 +14,7 @@ import ( "fmt" "time" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" goakt "github.com/tochemey/goakt/v3/actor" "google.golang.org/protobuf/proto" @@ -23,7 +23,7 @@ import ( // Sends a message to an actor identified by its name. // Uses either Tell or RemoteTell based on whether the actor is local or remote. func tell(ctx context.Context, actorName string, message proto.Message) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() addr, pid, err := _actorSystem.ActorOf(ctx, actorName) @@ -40,7 +40,7 @@ func tell(ctx context.Context, actorName string, message proto.Message) error { // Sends a message to an actor identified by its name, then waits for a response within the timeout duration. // Uses either Ask or RemoteAsk based on whether the actor is local or remote. func ask(ctx context.Context, actorName string, message proto.Message, timeout time.Duration) (response proto.Message, err error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() addr, pid, err := _actorSystem.ActorOf(ctx, actorName) diff --git a/runtime/actors/subscriber.go b/runtime/actors/subscriber.go index 2e690f104..f4297ccbe 100644 --- a/runtime/actors/subscriber.go +++ b/runtime/actors/subscriber.go @@ -16,6 +16,7 @@ import ( "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/messages" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/rs/xid" @@ -30,7 +31,7 @@ type agentEvent struct { } func SubscribeForAgentEvents(ctx context.Context, agentId string, update func(data []byte), done func()) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // Go directly to the database for the agent status, because we don't want subscribing to events to fail @@ -48,8 +49,9 @@ func SubscribeForAgentEvents(ctx context.Context, agentId string, update func(da done = func() {} } actor := &subscriptionActor{ - update: update, - done: done, + agentId: agentId, + update: update, + done: done, } // Spawn a subscription actor that is bound to the graphql subscription on this node. @@ -83,11 +85,15 @@ func SubscribeForAgentEvents(ctx context.Context, agentId string, update func(da unsubscribe := &goaktpb.Unsubscribe{Topic: topic} if err := subActor.Tell(ctx, _actorSystem.TopicActor(), unsubscribe); err != nil { - logger.Error(ctx, err).Msg("Failed to unsubscribe from topic") + const msg = "Failed to unsubscribe from topic" + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", agentId)) + logger.Error(ctx, err).Str("agent_id", agentId).Msg(msg) } if err := subActor.Shutdown(ctx); err != nil { - logger.Error(ctx, err).Msg("Failed to shut down subscription actor") + const msg = "Failed to shut down subscription actor" + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", agentId)) + logger.Error(ctx, err).Str("agent_id", agentId).Msg(msg) } }() @@ -95,8 +101,9 @@ func SubscribeForAgentEvents(ctx context.Context, agentId string, update func(da } type subscriptionActor struct { - update func(data []byte) - done func() + agentId string + update func(data []byte) + done func() } func (a *subscriptionActor) PreStart(ac *goakt.Context) error { @@ -110,7 +117,7 @@ func (a *subscriptionActor) PostStop(ac *goakt.Context) error { func (a *subscriptionActor) Receive(rc *goakt.ReceiveContext) { if msg, ok := rc.Message().(*messages.AgentEvent); ok { - span, _ := utils.NewSentrySpanForCurrentFunc(rc.Context()) + span, _ := sentryutils.NewSpanForCurrentFunc(rc.Context()) defer span.Finish() event := &agentEvent{ diff --git a/runtime/actors/wasmagent.go b/runtime/actors/wasmagent.go index 051ba2394..1d38803db 100644 --- a/runtime/actors/wasmagent.go +++ b/runtime/actors/wasmagent.go @@ -21,6 +21,7 @@ import ( "github.com/hypermodeinc/modus/runtime/messages" "github.com/hypermodeinc/modus/runtime/pluginmanager" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modus/runtime/wasmhost" @@ -48,7 +49,7 @@ func (a *wasmAgentActor) PreStart(ac *goakt.Context) error { a.sentryHub = sentry.CurrentHub().Clone() ctx = sentry.SetHubOnContext(ctx, a.sentryHub) - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() wasmExt := ac.Extension(wasmExtensionId).(*wasmExtension) @@ -76,7 +77,7 @@ func (a *wasmAgentActor) PreStart(ac *goakt.Context) error { func (a *wasmAgentActor) Receive(rc *goakt.ReceiveContext) { ctx := a.augmentContext(rc.Context(), rc.Self()) - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() switch msg := rc.Message().(type) { @@ -145,13 +146,15 @@ func (a *wasmAgentActor) Receive(rc *goakt.ReceiveContext) { func (a *wasmAgentActor) PostStop(ac *goakt.Context) error { ctx := ac.Context() ctx = sentry.SetHubOnContext(ctx, a.sentryHub) - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // suspend the agent if it's not already suspended or terminated if a.status != AgentStatusSuspended && a.status != AgentStatusTerminated { if err := a.suspendAgent(ctx); err != nil { - logger.Error(ctx, err).Msg("Error suspending agent.") + const msg = "Error suspending agent." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("agent_id", a.agentId)) + logger.Error(ctx, err).Str("agent_id", a.agentId).Msg(msg) // don't return on error - we'll still try to deactivate the agent } } @@ -164,7 +167,7 @@ func (a *wasmAgentActor) PostStop(ac *goakt.Context) error { } func (a *wasmAgentActor) handleAgentRequest(ctx context.Context, rc *goakt.ReceiveContext, msg *messages.AgentRequest) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if a.status != AgentStatusRunning { @@ -200,7 +203,11 @@ func (a *wasmAgentActor) handleAgentRequest(ctx context.Context, rc *goakt.Recei response.Data = result default: err := fmt.Errorf("unexpected result type: %T", result) - logger.Error(ctx, err).Msg("Error handling message.") + const errMsg = "Error handling message." + sentryutils.CaptureError(ctx, err, errMsg, + sentryutils.WithData("agent_id", a.agentId), + sentryutils.WithData("msg_name", msg.Name)) + logger.Error(ctx, err).Str("agent_id", a.agentId).Str("msg_name", msg.Name).Msg(errMsg) return err } } @@ -215,7 +222,9 @@ func (a *wasmAgentActor) handleAgentRequest(ctx context.Context, rc *goakt.Recei // save the state after handling the message to ensure the state is up to date in case of hard termination if err := a.saveState(ctx); err != nil { - logger.Error(ctx, err).Msg("Error saving agent state.") + const errMsg = "Error saving agent state." + sentryutils.CaptureError(ctx, err, errMsg, sentryutils.WithData("agent_id", a.agentId)) + logger.Error(ctx, err).Str("agent_id", a.agentId).Msg(errMsg) } return nil @@ -226,7 +235,7 @@ func (a *wasmAgentActor) updateStatus(ctx context.Context, status AgentStatus) e return nil } - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() a.status = status @@ -244,7 +253,7 @@ func (a *wasmAgentActor) updateStatus(ctx context.Context, status AgentStatus) e } func (a *wasmAgentActor) saveState(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() var data string @@ -270,7 +279,7 @@ func (a *wasmAgentActor) saveState(ctx context.Context) error { } func (a *wasmAgentActor) restoreState(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if a.module == nil { @@ -312,7 +321,7 @@ func (a *wasmAgentActor) augmentContext(ctx context.Context, pid *goakt.PID) con } func (a *wasmAgentActor) activateAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if plugin, found := pluginmanager.GetPluginByName(a.pluginName); found { @@ -342,7 +351,7 @@ func (a *wasmAgentActor) activateAgent(ctx context.Context) error { } func (a *wasmAgentActor) deactivateAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if err := a.module.Close(ctx); err != nil { @@ -353,7 +362,7 @@ func (a *wasmAgentActor) deactivateAgent(ctx context.Context) error { } func (a *wasmAgentActor) startAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Info(ctx).Msg("Starting agent.") @@ -374,7 +383,7 @@ func (a *wasmAgentActor) startAgent(ctx context.Context) error { } func (a *wasmAgentActor) resumeAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Info(ctx).Msg("Resuming agent.") @@ -395,7 +404,7 @@ func (a *wasmAgentActor) resumeAgent(ctx context.Context) error { } func (a *wasmAgentActor) suspendAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Info(ctx).Msg("Suspending agent.") @@ -416,7 +425,7 @@ func (a *wasmAgentActor) suspendAgent(ctx context.Context) error { } func (a *wasmAgentActor) stopAgent(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() logger.Info(ctx).Msg("Stopping agent.") @@ -437,7 +446,7 @@ func (a *wasmAgentActor) stopAgent(ctx context.Context) error { } func (a *wasmAgentActor) callEventHandler(ctx context.Context, action agentEventAction) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() fnInfo, err := a.host.GetFunctionInfo("_modus_agent_handle_event") @@ -454,7 +463,7 @@ func (a *wasmAgentActor) callEventHandler(ctx context.Context, action agentEvent } func (a *wasmAgentActor) getAgentState(ctx context.Context) (*string, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() fnInfo, err := a.host.GetFunctionInfo("_modus_agent_get_state") @@ -483,7 +492,7 @@ func (a *wasmAgentActor) getAgentState(ctx context.Context) (*string, error) { } func (a *wasmAgentActor) setAgentState(ctx context.Context, data *string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() fnInfo, err := a.host.GetFunctionInfo("_modus_agent_set_state") diff --git a/runtime/app/app.go b/runtime/app/app.go index e7549ef89..16417115d 100644 --- a/runtime/app/app.go +++ b/runtime/app/app.go @@ -11,9 +11,7 @@ package app import ( "os" - "path" "path/filepath" - "runtime" "strings" "sync" @@ -76,17 +74,6 @@ func SetShuttingDown() { shuttingDown = true } -// GetRootSourcePath returns the root path of the source code. -// It is used to trim the paths in stack traces when included in utils. -func GetRootSourcePath() string { - _, filename, _, ok := runtime.Caller(0) - if !ok { - return "" - } - - return path.Dir(path.Dir(filename)) + "/" -} - func ModusHomeDir() string { modusHome := os.Getenv("MODUS_HOME") if modusHome == "" { diff --git a/runtime/app/app_test.go b/runtime/app/app_test.go index 16610fd7c..b5c247e0a 100644 --- a/runtime/app/app_test.go +++ b/runtime/app/app_test.go @@ -11,22 +11,12 @@ package app_test import ( "os" - "path" "testing" "github.com/fatih/color" "github.com/hypermodeinc/modus/runtime/app" ) -func TestGetRootSourcePath(t *testing.T) { - cwd, _ := os.Getwd() - expectedPath := path.Dir(cwd) + "/" - actualPath := app.GetRootSourcePath() - - if actualPath != expectedPath { - t.Errorf("Expected path: %s, but got: %s", expectedPath, actualPath) - } -} func TestIsShuttingDown(t *testing.T) { if app.IsShuttingDown() { t.Errorf("Expected initial state to be not shutting down") diff --git a/runtime/db/agentstate.go b/runtime/db/agentstate.go index 47f7d9206..9eaaf9601 100644 --- a/runtime/db/agentstate.go +++ b/runtime/db/agentstate.go @@ -14,6 +14,7 @@ import ( "fmt" "time" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modusgraph" @@ -62,7 +63,7 @@ func QueryActiveAgents(ctx context.Context) ([]AgentState, error) { } func writeAgentStateToModusDB(ctx context.Context, state AgentState) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() gid, _, _, err := modusgraph.Upsert(ctx, GlobalModusDbEngine, state) @@ -72,7 +73,7 @@ func writeAgentStateToModusDB(ctx context.Context, state AgentState) error { } func updateAgentStatusInModusDB(ctx context.Context, id string, status string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // TODO: this should just be an update in a single operation @@ -89,7 +90,7 @@ func updateAgentStatusInModusDB(ctx context.Context, id string, status string) e } func getAgentStateFromModusDB(ctx context.Context, id string) (*AgentState, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() _, result, err := modusgraph.Get[AgentState](ctx, GlobalModusDbEngine, modusgraph.ConstrainedField{ @@ -104,7 +105,7 @@ func getAgentStateFromModusDB(ctx context.Context, id string) (*AgentState, erro } func queryActiveAgentsFromModusDB(ctx context.Context) ([]AgentState, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() _, results, err := modusgraph.Query[AgentState](ctx, GlobalModusDbEngine, modusgraph.QueryParams{ @@ -131,7 +132,7 @@ func queryActiveAgentsFromModusDB(ctx context.Context) ([]AgentState, error) { } func writeAgentStateToPostgresDB(ctx context.Context, state AgentState) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() const query = "INSERT INTO agents (id, name, status, data, updated) VALUES ($1, $2, $3, $4, $5) " + @@ -149,7 +150,7 @@ func writeAgentStateToPostgresDB(ctx context.Context, state AgentState) error { } func updateAgentStatusInPostgresDB(ctx context.Context, id string, status string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() const query = "UPDATE agents SET status = $2, updated = $3 WHERE id = $1" @@ -167,7 +168,7 @@ func updateAgentStatusInPostgresDB(ctx context.Context, id string, status string } func getAgentStateFromPostgresDB(ctx context.Context, id string) (*AgentState, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() const query = "SELECT id, name, status, data, updated FROM agents WHERE id = $1" @@ -191,7 +192,7 @@ func getAgentStateFromPostgresDB(ctx context.Context, id string) (*AgentState, e } func queryActiveAgentsFromPostgresDB(ctx context.Context) ([]AgentState, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() const query = "SELECT id, name, status, data, updated FROM agents " + diff --git a/runtime/db/db.go b/runtime/db/db.go index e8b9c14fe..b150a4ecd 100644 --- a/runtime/db/db.go +++ b/runtime/db/db.go @@ -19,7 +19,7 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" @@ -49,16 +49,15 @@ func Stop(ctx context.Context) { pool.Close() } -func logDbWarningOrError(ctx context.Context, err error, msg string) { +func logDbError(ctx context.Context, err error, msg string) { + sentryutils.CaptureError(ctx, err, msg) if _, ok := err.(*pgconn.ConnectError); ok { - logger.Warn(ctx, err).Msgf("Database connection error. %s", msg) + logger.Error(ctx, err).Msgf("Database connection error. %s", msg) } else if errors.Is(err, errDbNotConfigured) { if !useModusDB() { - logger.Warn(ctx).Msgf("Database has not been configured. %s", msg) + logger.Error(ctx).Msgf("Database has not been configured. %s", msg) } } else { - // not really an error, but we log it as such - // but user-visible so it doesn't flag in Sentry logger.Error(ctx, err).Bool("user_visible", true).Msg(msg) } } @@ -86,7 +85,7 @@ func GetTx(ctx context.Context) (pgx.Tx, error) { } func WithTx(ctx context.Context, fn func(pgx.Tx) error) error { - span, ctx := utils.NewSentrySpanForCallingFunc(ctx) + span, ctx := sentryutils.NewSpanForCallingFunc(ctx) defer span.Finish() tx, err := GetTx(ctx) diff --git a/runtime/db/inferencehistory.go b/runtime/db/inferencehistory.go index 4def4725f..b5b62b4cf 100644 --- a/runtime/db/inferencehistory.go +++ b/runtime/db/inferencehistory.go @@ -190,7 +190,7 @@ func WritePluginInfo(ctx context.Context, plugin *plugins.Plugin) { if useModusDB() { err := writePluginInfoToModusdb(ctx, plugin) if err != nil { - logDbWarningOrError(ctx, err, "Plugin info not written to modusgraph.") + logDbError(ctx, err, "Plugin info not written to modusgraph.") } return } @@ -245,7 +245,7 @@ ON CONFLICT (build_id) DO NOTHING`, }) if err != nil { - logDbWarningOrError(ctx, err, "Plugin info not written to database.") + logDbError(ctx, err, "Plugin info not written to database.") } } @@ -312,7 +312,7 @@ func WriteInferenceHistoryToDB(ctx context.Context, batch []inferenceHistory) { if useModusDB() { err := writeInferenceHistoryToModusDb(ctx, batch) if err != nil { - logDbWarningOrError(ctx, err, "Inference history not written to modusgraph.") + logDbError(ctx, err, "Inference history not written to modusgraph.") } return } @@ -355,7 +355,7 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) }) if err != nil { - logDbWarningOrError(ctx, err, "Inference history not written to database.") + logDbError(ctx, err, "Inference history not written to database.") } } diff --git a/runtime/db/modusdb.go b/runtime/db/modusdb.go index fa5e3bb90..b8857fe2e 100644 --- a/runtime/db/modusdb.go +++ b/runtime/db/modusdb.go @@ -13,11 +13,13 @@ import ( "bufio" "context" "errors" + "fmt" "os" "path/filepath" "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modusgraph" ) @@ -34,13 +36,19 @@ func InitModusDb(ctx context.Context) { if filepath.Base(appPath) == "build" { // this keeps the data directory outside of the build directory dataDir = filepath.Join(appPath, "..", ".modusdb") - addToGitIgnore(ctx, filepath.Dir(appPath), ".modusdb/") + if err := addToGitIgnore(ctx, filepath.Dir(appPath), ".modusdb/"); err != nil { + const msg = "Failed to add .modusdb to .gitignore" + sentryutils.CaptureWarning(ctx, err, msg) + logger.Warn(ctx, err).Msg(msg) + } } else { dataDir = filepath.Join(appPath, ".modusdb") } if eng, err := modusgraph.NewEngine(modusgraph.NewDefaultConfig(dataDir)); err != nil { - logger.Fatal(ctx, err).Msg("Failed to initialize the local modusGraph database.") + const msg = "Failed to initialize the local modusGraph database." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } else { GlobalModusDbEngine = eng } @@ -52,39 +60,39 @@ func CloseModusDb(ctx context.Context) { } } -func addToGitIgnore(ctx context.Context, rootPath, contents string) { +func addToGitIgnore(ctx context.Context, rootPath, contents string) error { gitIgnorePath := filepath.Join(rootPath, ".gitignore") // if .gitignore file does not exist, create it and add contents to it if _, err := os.Stat(gitIgnorePath); errors.Is(err, os.ErrNotExist) { if err := os.WriteFile(gitIgnorePath, []byte(contents+"\n"), 0644); err != nil { - logger.Error(ctx, err).Msg("Failed to create .gitignore file.") + return fmt.Errorf("failed to create .gitignore file: %w", err) } - return + return nil } // check if contents are already in the .gitignore file file, err := os.Open(gitIgnorePath) if err != nil { - logger.Error(ctx, err).Msg("Failed to open .gitignore file.") - return + return fmt.Errorf("failed to open .gitignore file: %w", err) } - defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { if scanner.Text() == contents { - return // found + file.Close() + return nil // found } } + file.Close() // contents are not in the file, so append them file, err = os.OpenFile(gitIgnorePath, os.O_APPEND|os.O_WRONLY, 0644) if err != nil { - logger.Error(ctx, err).Msg("Failed to open .gitignore file.") - return + return fmt.Errorf("failed to open .gitignore file: %w", err) } defer file.Close() if _, err := file.WriteString("\n" + contents + "\n"); err != nil { - logger.Error(ctx, err).Msg("Failed to append " + contents + " to .gitignore file.") + return fmt.Errorf("failed to append to .gitignore file: %w", err) } + return nil } diff --git a/runtime/envfiles/envfilemonitor.go b/runtime/envfiles/envfilemonitor.go index e4a8fcfe2..c54790920 100644 --- a/runtime/envfiles/envfilemonitor.go +++ b/runtime/envfiles/envfilemonitor.go @@ -13,6 +13,7 @@ import ( "context" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/storage" ) @@ -23,7 +24,9 @@ func MonitorEnvFiles(ctx context.Context) { if len(errors) == 0 { err := LoadEnvFiles(ctx) if err != nil { - logger.Error(ctx, err).Msg("Failed to load env files.") + const msg = "Failed to load env files." + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) } } } diff --git a/runtime/graphql/datasource/planner.go b/runtime/graphql/datasource/planner.go index 4620fef6c..2d97ddc6c 100644 --- a/runtime/graphql/datasource/planner.go +++ b/runtime/graphql/datasource/planner.go @@ -16,6 +16,7 @@ import ( "strings" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/tidwall/gjson" @@ -124,7 +125,9 @@ func (p *modusDataSourcePlanner) EnterField(ref int) { p.template.fieldInfo = f p.template.functionName = config.FieldsToFunctions[f.Name] if err := p.captureInputData(ref); err != nil { - logger.Error(p.ctx, err).Msg("Error capturing input data.") + const msg = "Error capturing graphql input data." + sentryutils.CaptureError(p.ctx, err, msg) + logger.Error(p.ctx, err).Msg(msg) return } } @@ -250,7 +253,9 @@ func (p *modusDataSourcePlanner) getInputTemplate() (string, error) { func (p *modusDataSourcePlanner) ConfigureFetch() resolve.FetchConfiguration { input, err := p.getInputTemplate() if err != nil { - logger.Error(p.ctx, err).Msg("Error creating input template for Modus data source.") + const msg = "Error creating input template for graphql data source." + sentryutils.CaptureError(p.ctx, err, msg) + logger.Error(p.ctx, err).Msg(msg) return resolve.FetchConfiguration{} } @@ -272,7 +277,9 @@ func (p *modusDataSourcePlanner) ConfigureFetch() resolve.FetchConfiguration { func (p *modusDataSourcePlanner) ConfigureSubscription() plan.SubscriptionConfiguration { input, err := p.getInputTemplate() if err != nil { - logger.Error(p.ctx, err).Msg("Error creating input template for Modus data source.") + const msg = "Error creating input template for graphql data source." + sentryutils.CaptureError(p.ctx, err, msg) + logger.Error(p.ctx, err).Msg(msg) return plan.SubscriptionConfiguration{} } diff --git a/runtime/graphql/engine/engine.go b/runtime/graphql/engine/engine.go index 21d536837..24d229701 100644 --- a/runtime/graphql/engine/engine.go +++ b/runtime/graphql/engine/engine.go @@ -24,6 +24,7 @@ import ( "github.com/hypermodeinc/modus/runtime/graphql/schemagen" "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modus/runtime/wasmhost" @@ -50,7 +51,7 @@ func setEngine(engine *engine.ExecutionEngine) { } func Activate(ctx context.Context, plugin *plugins.Plugin) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() schema, cfg, err := generateSchema(ctx, plugin.Metadata) @@ -73,7 +74,7 @@ func Activate(ctx context.Context, plugin *plugins.Plugin) error { } func generateSchema(ctx context.Context, md *metadata.Metadata) (*gql.Schema, *datasource.ModusDataSourceConfig, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() generated, err := schemagen.GetGraphQLSchema(ctx, md) @@ -104,7 +105,7 @@ func generateSchema(ctx context.Context, md *metadata.Metadata) (*gql.Schema, *d } func getDatasourceConfig(ctx context.Context, schema *gql.Schema, cfg *datasource.ModusDataSourceConfig) (plan.DataSourceConfiguration[datasource.ModusDataSourceConfig], error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() queryTypeName := schema.QueryTypeName() @@ -166,7 +167,7 @@ func getChildNodes(fieldNames []string, schema *gql.Schema, typeName string) []p } func makeEngine(ctx context.Context, schema *gql.Schema, datasourceConfig plan.DataSourceConfiguration[datasource.ModusDataSourceConfig]) (*engine.ExecutionEngine, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() engineConfig := engine.NewConfiguration(schema) @@ -183,7 +184,7 @@ func makeEngine(ctx context.Context, schema *gql.Schema, datasourceConfig plan.D } func getTypeFields(ctx context.Context, s *gql.Schema, typeName string) []string { - span, _ := utils.NewSentrySpanForCurrentFunc(ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() doc := s.Document() diff --git a/runtime/graphql/graphql.go b/runtime/graphql/graphql.go index e52ced73d..701521ad2 100644 --- a/runtime/graphql/graphql.go +++ b/runtime/graphql/graphql.go @@ -22,6 +22,7 @@ import ( "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/manifestdata" "github.com/hypermodeinc/modus/runtime/pluginmanager" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/timezones" "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modus/runtime/wasmhost" @@ -141,6 +142,7 @@ func handleGraphQLRequest(w http.ResponseWriter, r *http.Request) { resultWriter := gql.NewEngineResultWriter() if operationType, err := gqlRequest.OperationType(); err != nil { msg := "Failed to determine operation type from GraphQL request." + sentryutils.CaptureError(ctx, err, msg) logger.Error(ctx, err).Msg(msg) http.Error(w, msg, http.StatusBadRequest) return @@ -187,6 +189,7 @@ func handleGraphQLRequest(w http.ResponseWriter, r *http.Request) { if len(report.InternalErrors) > 0 { // Log internal errors, but don't return them to the client msg := "Failed to execute GraphQL operation." + sentryutils.CaptureError(ctx, err, msg) logger.Error(ctx, err).Msg(msg) http.Error(w, msg, http.StatusInternalServerError) return @@ -214,6 +217,7 @@ func handleGraphQLRequest(w http.ResponseWriter, r *http.Request) { } } else { msg := "Failed to execute GraphQL operation." + sentryutils.CaptureError(ctx, err, msg) logger.Error(ctx, err).Msg(msg) http.Error(w, fmt.Sprintf("%s\n%v", msg, err), http.StatusInternalServerError) } @@ -228,6 +232,7 @@ func handleGraphQLRequest(w http.ResponseWriter, r *http.Request) { if response, err := addOutputToResponse(resultWriter.Bytes(), xsync.ToPlainMap(output)); err != nil { msg := "Failed to add function output to response." + sentryutils.CaptureError(ctx, err, msg) logger.Error(ctx, err).Msg(msg) http.Error(w, fmt.Sprintf("%s\n%v", msg, err), http.StatusInternalServerError) } else { diff --git a/runtime/graphql/schemagen/schemagen.go b/runtime/graphql/schemagen/schemagen.go index c48d499d5..8f361b1d0 100644 --- a/runtime/graphql/schemagen/schemagen.go +++ b/runtime/graphql/schemagen/schemagen.go @@ -21,6 +21,7 @@ import ( "github.com/hypermodeinc/modus/lib/metadata" "github.com/hypermodeinc/modus/runtime/langsupport" "github.com/hypermodeinc/modus/runtime/languages" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" ) @@ -31,7 +32,7 @@ type GraphQLSchema struct { } func GetGraphQLSchema(ctx context.Context, md *metadata.Metadata) (*GraphQLSchema, error) { - span, _ := utils.NewSentrySpanForCurrentFunc(ctx) + span, _ := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() lang, err := languages.GetLanguageForSDK(md.SDK) diff --git a/runtime/hostfunctions/system.go b/runtime/hostfunctions/system.go index 7fd530955..ec39ee537 100644 --- a/runtime/hostfunctions/system.go +++ b/runtime/hostfunctions/system.go @@ -16,6 +16,7 @@ import ( "time" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/timezones" "github.com/hypermodeinc/modus/runtime/utils" ) @@ -61,13 +62,17 @@ func GetTimeInZone(ctx context.Context, tz *string) *string { } else if tz, ok := ctx.Value(utils.TimeZoneContextKey).(string); ok { zoneId = tz } else { - logger.Error(ctx).Msg("Time zone not specified.") + const msg = "Time zone not specified." + sentryutils.CaptureError(ctx, nil, msg) + logger.Error(ctx).Msg(msg) return nil } loc, err := timezones.GetLocation(ctx, zoneId) if err != nil { - logger.Error(ctx, err).Str("tz", zoneId).Msg("Failed to get time zone location.") + const msg = "Failed to get time zone location." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("tz", zoneId)) + logger.Error(ctx, err).Str("tz", zoneId).Msg(msg) return nil } @@ -77,16 +82,22 @@ func GetTimeInZone(ctx context.Context, tz *string) *string { func GetTimeZoneData(ctx context.Context, tz, format *string) []byte { if tz == nil { - logger.Error(ctx).Msg("Time zone not specified.") + const msg = "Time zone not specified." + sentryutils.CaptureError(ctx, nil, msg) + logger.Error(ctx).Msg(msg) return nil } if format == nil { - logger.Error(ctx).Msg("Time zone format not specified.") + const msg = "Time zone format not specified." + sentryutils.CaptureError(ctx, nil, msg) + logger.Error(ctx).Msg(msg) return nil } data, err := timezones.GetTimeZoneData(ctx, *tz, *format) if err != nil { - logger.Error(ctx, err).Str("tz", *tz).Msg("Failed to get time zone data.") + const msg = "Failed to get time zone data." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("tz", *tz), sentryutils.WithData("format", *format)) + logger.Error(ctx, err).Str("tz", *tz).Str("format", *format).Msg(msg) return nil } return data diff --git a/runtime/httpserver/health.go b/runtime/httpserver/health.go index c747f7f63..cdd36d896 100644 --- a/runtime/httpserver/health.go +++ b/runtime/httpserver/health.go @@ -15,9 +15,12 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" ) +const msg = "Failed to serialize health check response." + var healthHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data := []utils.KeyValuePair{ @@ -32,7 +35,9 @@ var healthHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request jsonBytes, err := utils.MakeJsonObject(data, true) if err != nil { - logger.Error(r.Context(), err).Msg("Failed to serialize health check response.") + var ctx = r.Context() + logger.Error(ctx, err).Msg(msg) + sentryutils.CaptureError(ctx, err, msg) w.WriteHeader(http.StatusInternalServerError) return } diff --git a/runtime/httpserver/server.go b/runtime/httpserver/server.go index ca350d8cf..069b0c3d0 100644 --- a/runtime/httpserver/server.go +++ b/runtime/httpserver/server.go @@ -29,6 +29,7 @@ import ( "github.com/hypermodeinc/modus/runtime/manifestdata" "github.com/hypermodeinc/modus/runtime/metrics" "github.com/hypermodeinc/modus/runtime/middleware" + "github.com/hypermodeinc/modus/runtime/sentryutils" sentryhttp "github.com/getsentry/sentry-go/http" @@ -85,7 +86,9 @@ func startHttpServer(ctx context.Context, mux http.Handler, addresses ...string) err := server.ListenAndServe() app.SetShuttingDown() if err != nil && !errors.Is(err, http.ErrServerClosed) { - logger.Fatal(ctx, err).Msg("HTTP server error. Exiting.") + const msg = "HTTP server error. Exiting." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } shutdownChan <- true }() @@ -114,7 +117,9 @@ func startHttpServer(ctx context.Context, mux http.Handler, addresses ...string) defer shutdownRelease() server.RegisterOnShutdown(graphql.CancelSubscriptions) if err := server.Shutdown(shutdownCtx); err != nil { - logger.Fatal(ctx, err).Msg("HTTP server shutdown error.") + const msg = "HTTP server shutdown error." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } } diff --git a/runtime/langsupport/executionplan.go b/runtime/langsupport/executionplan.go index 585e292c0..b654bbda8 100644 --- a/runtime/langsupport/executionplan.go +++ b/runtime/langsupport/executionplan.go @@ -14,7 +14,9 @@ import ( "fmt" "runtime/debug" + "github.com/getsentry/sentry-go" "github.com/hypermodeinc/modus/lib/metadata" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" wasm "github.com/tetratelabs/wazero/api" @@ -27,10 +29,11 @@ type ExecutionPlan interface { ResultHandlers() []TypeHandler UseResultIndirection() bool HasDefaultParameters() bool + PluginName() string InvokeFunction(ctx context.Context, wa WasmAdapter, parameters map[string]any) (result any, err error) } -func NewExecutionPlan(fnDef wasm.FunctionDefinition, fnMeta *metadata.Function, paramHandlers, resultHandlers []TypeHandler, indirectResultSize uint32) ExecutionPlan { +func NewExecutionPlan(fnDef wasm.FunctionDefinition, fnMeta *metadata.Function, paramHandlers, resultHandlers []TypeHandler, indirectResultSize uint32, pluginName string) ExecutionPlan { hasDefaultParameters := false for _, p := range fnMeta.Parameters { if p.Default != nil { @@ -39,7 +42,7 @@ func NewExecutionPlan(fnDef wasm.FunctionDefinition, fnMeta *metadata.Function, } } - return &executionPlan{fnDef, fnMeta, paramHandlers, resultHandlers, indirectResultSize, hasDefaultParameters} + return &executionPlan{fnDef, fnMeta, paramHandlers, resultHandlers, indirectResultSize, hasDefaultParameters, pluginName} } type executionPlan struct { @@ -49,6 +52,7 @@ type executionPlan struct { resultHandlers []TypeHandler indirectResultSize uint32 hasDefaultParameters bool + pluginName string } func (p *executionPlan) FnDefinition() wasm.FunctionDefinition { @@ -75,21 +79,34 @@ func (p *executionPlan) HasDefaultParameters() bool { return p.hasDefaultParameters } +func (p *executionPlan) PluginName() string { + return p.pluginName +} + func (plan *executionPlan) InvokeFunction(ctx context.Context, wa WasmAdapter, parameters map[string]any) (result any, err error) { + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) + defer span.Finish() + + fnName := plan.FnMetadata().Name + fullName := plan.PluginName() + "." + fnName + + scope, done := sentryutils.NewScope(ctx) + defer done() + sentryutils.AddTextBreadcrumbToScope(scope, "Starting wasm function: "+fullName) + defer sentryutils.AddTextBreadcrumbToScope(scope, "Finished wasm function: "+fullName) + // Recover from panics and convert them to errors defer func() { if r := recover(); r != nil { - err = utils.ConvertToError(r) - utils.CaptureError(ctx, err) + sentryutils.Recover(ctx, r) + err = utils.ConvertToError(r) // return the error to the caller if utils.DebugModeEnabled() { debug.PrintStack() } - } }() // Get the wasm function - fnName := plan.FnMetadata().Name fn := wa.GetFunction(fnName) if fn == nil { return nil, fmt.Errorf("function %s not found in wasm module", fnName) @@ -115,7 +132,10 @@ func (plan *executionPlan) InvokeFunction(ctx context.Context, wa WasmAdapter, p } // Call the function - res, err := fn.Call(ctx, params...) + fnSpan := sentry.StartSpan(ctx, "wasm_function") + fnSpan.Description = fullName + res, err := fn.Call(span.Context(), params...) + fnSpan.Finish() if err != nil { return nil, err } diff --git a/runtime/languages/assemblyscript/planner.go b/runtime/languages/assemblyscript/planner.go index fb70d2cdf..8949c287f 100644 --- a/runtime/languages/assemblyscript/planner.go +++ b/runtime/languages/assemblyscript/planner.go @@ -15,7 +15,7 @@ import ( "github.com/hypermodeinc/modus/lib/metadata" "github.com/hypermodeinc/modus/runtime/langsupport" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" wasm "github.com/tetratelabs/wazero/api" ) @@ -78,7 +78,7 @@ func (p *planner) GetHandler(ctx context.Context, typeName string) (langsupport. } func (p *planner) GetPlan(ctx context.Context, fnMeta *metadata.Function, fnDef wasm.FunctionDefinition) (langsupport.ExecutionPlan, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() paramHandlers := make([]langsupport.TypeHandler, len(fnMeta.Parameters)) @@ -99,6 +99,7 @@ func (p *planner) GetPlan(ctx context.Context, fnMeta *metadata.Function, fnDef resultHandlers[i] = handler } - plan := langsupport.NewExecutionPlan(fnDef, fnMeta, paramHandlers, resultHandlers, 0) + pluginName := p.metadata.Plugin + plan := langsupport.NewExecutionPlan(fnDef, fnMeta, paramHandlers, resultHandlers, 0, pluginName) return plan, nil } diff --git a/runtime/languages/golang/planner.go b/runtime/languages/golang/planner.go index 50fdcd96a..6d1a65e08 100644 --- a/runtime/languages/golang/planner.go +++ b/runtime/languages/golang/planner.go @@ -15,7 +15,7 @@ import ( "github.com/hypermodeinc/modus/lib/metadata" "github.com/hypermodeinc/modus/runtime/langsupport" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" wasm "github.com/tetratelabs/wazero/api" ) @@ -98,7 +98,7 @@ func (p *planner) GetHandler(ctx context.Context, typeName string) (langsupport. } func (p *planner) GetPlan(ctx context.Context, fnMeta *metadata.Function, fnDef wasm.FunctionDefinition) (langsupport.ExecutionPlan, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() paramHandlers := make([]langsupport.TypeHandler, len(fnMeta.Parameters)) @@ -124,7 +124,8 @@ func (p *planner) GetPlan(ctx context.Context, fnMeta *metadata.Function, fnDef return nil, err } - plan := langsupport.NewExecutionPlan(fnDef, fnMeta, paramHandlers, resultHandlers, indirectResultSize) + pluginName := p.metadata.Plugin + plan := langsupport.NewExecutionPlan(fnDef, fnMeta, paramHandlers, resultHandlers, indirectResultSize, pluginName) return plan, nil } diff --git a/runtime/logger/logger.go b/runtime/logger/logger.go index 2055def01..982260ecc 100644 --- a/runtime/logger/logger.go +++ b/runtime/logger/logger.go @@ -123,14 +123,8 @@ func Warn(ctx context.Context, errs ...error) *zerolog.Event { if err == nil { return Get(ctx).Warn() } - utils.CaptureWarning(ctx, err) return Get(ctx).Warn().Err(err) default: - for _, err := range errs { - if err != nil { - utils.CaptureWarning(ctx, err) - } - } return Get(ctx).Warn().Errs("errors", errs) } } @@ -144,14 +138,8 @@ func Error(ctx context.Context, errs ...error) *zerolog.Event { if err == nil { return Get(ctx).Error() } - utils.CaptureError(ctx, err) return Get(ctx).Err(err) default: - for _, err := range errs { - if err != nil { - utils.CaptureError(ctx, err) - } - } return Get(ctx).Error().Errs("errors", errs) } } @@ -165,14 +153,8 @@ func Fatal(ctx context.Context, errs ...error) *zerolog.Event { if err == nil { return Get(ctx).Fatal() } - utils.CaptureError(ctx, err) return Get(ctx).Fatal().Err(err) default: - for _, err := range errs { - if err != nil { - utils.CaptureError(ctx, err) - } - } return Get(ctx).Fatal().Errs("errors", errs) } } diff --git a/runtime/main.go b/runtime/main.go index 9d3eca6ba..87775160a 100644 --- a/runtime/main.go +++ b/runtime/main.go @@ -16,8 +16,8 @@ import ( "github.com/hypermodeinc/modus/runtime/envfiles" "github.com/hypermodeinc/modus/runtime/httpserver" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/services" - "github.com/hypermodeinc/modus/runtime/utils" ) func main() { @@ -41,8 +41,8 @@ func main() { } // Initialize Sentry (if enabled) - utils.InitializeSentry() - defer utils.FinalizeSentry() + sentryutils.InitializeSentry() + defer sentryutils.CloseSentry() // Get the main handler for the HTTP server before starting the services, // so it can register the endpoints as the manifest is loaded. diff --git a/runtime/manifestdata/manifestdata.go b/runtime/manifestdata/manifestdata.go index 534d0c474..623216688 100644 --- a/runtime/manifestdata/manifestdata.go +++ b/runtime/manifestdata/manifestdata.go @@ -15,8 +15,8 @@ import ( "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/storage" - "github.com/hypermodeinc/modus/runtime/utils" ) const manifestFileName = "modus.json" @@ -44,7 +44,9 @@ func MonitorManifestFile(ctx context.Context) { } if err := loadManifest(ctx); err != nil { - logger.Error(ctx, err).Str("filename", file.Name).Msg("Failed to load manifest file.") + const msg = "Failed to load manifest file." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("filename", file.Name)) + logger.Error(ctx, err).Str("filename", file.Name).Msg(msg) return err } @@ -58,7 +60,9 @@ func MonitorManifestFile(ctx context.Context) { if file.Name == manifestFileName { logger.Warn(ctx).Str("filename", file.Name).Msg("Manifest file removed.") if err := unloadManifest(ctx); err != nil { - logger.Error(ctx, err).Str("filename", file.Name).Msg("Failed to unload manifest file.") + const msg = "Failed to unload manifest file." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("filename", file.Name)) + logger.Error(ctx, err).Str("filename", file.Name).Msg(msg) return err } } @@ -68,7 +72,7 @@ func MonitorManifestFile(ctx context.Context) { } func loadManifest(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() bytes, err := storage.GetFileContents(ctx, manifestFileName) diff --git a/runtime/middleware/jwt.go b/runtime/middleware/jwt.go index 4770a7fa2..9a8b7ca24 100644 --- a/runtime/middleware/jwt.go +++ b/runtime/middleware/jwt.go @@ -19,6 +19,7 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/envfiles" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/golang-jwt/jwt/v5" @@ -46,10 +47,13 @@ func initKeys(ctx context.Context) { if publicPemKeysJson != "" { keys, err := publicPemKeysJsonToKeys(publicPemKeysJson) if err != nil { + const msg = "Auth PEM public keys deserializing error" + sentryutils.CaptureError(ctx, err, msg) if app.IsDevEnvironment() { - logger.Fatal(ctx, err).Msg("Auth PEM public keys deserializing error") + logger.Fatal(ctx, err).Msg(msg) + } else { + logger.Error(ctx, err).Msg(msg) } - logger.Error(ctx, err).Msg("Auth PEM public keys deserializing error") return } globalAuthKeys.setPemPublicKeys(keys) @@ -57,10 +61,13 @@ func initKeys(ctx context.Context) { if jwksEndpointsJson != "" { keys, err := jwksEndpointsJsonToKeys(ctx, jwksEndpointsJson) if err != nil { + const msg = "Auth JWKS public keys deserializing error" + sentryutils.CaptureError(ctx, err, msg) if app.IsDevEnvironment() { - logger.Fatal(ctx, err).Msg("Auth JWKS public keys deserializing error") + logger.Fatal(ctx, err).Msg(msg) + } else { + logger.Error(ctx, err).Msg(msg) } - logger.Error(ctx, err).Msg("Auth JWKS public keys deserializing error") return } globalAuthKeys.setJwksPublicKeys(keys) @@ -160,7 +167,9 @@ func HandleJWT(next http.Handler) http.Handler { func addClaimsToContext(ctx context.Context, claims jwt.MapClaims) context.Context { claimsJson, err := utils.JsonSerialize(claims) if err != nil { - logger.Error(ctx, err).Msg("JWT claims serialization error") + const msg = "JWT claims serialization error" + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) return ctx } return context.WithValue(ctx, jwtClaims, string(claimsJson)) diff --git a/runtime/models/bedrock.go b/runtime/models/bedrock.go index a0ec09914..5c2b7c06b 100644 --- a/runtime/models/bedrock.go +++ b/runtime/models/bedrock.go @@ -24,7 +24,7 @@ package models // func invokeAwsBedrockModel(ctx context.Context, model *manifest.ModelInfo, input string) (output string, err error) { -// span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) +// span, ctx := sentryutils.NewSentrySpanForCurrentFunc(ctx) // defer span.Finish() // // NOTE: Bedrock support is experimental, and not advertised to users. diff --git a/runtime/models/models.go b/runtime/models/models.go index 85bb15f80..a8340bbd9 100644 --- a/runtime/models/models.go +++ b/runtime/models/models.go @@ -22,6 +22,7 @@ import ( "github.com/hypermodeinc/modus/runtime/httpclient" "github.com/hypermodeinc/modus/runtime/manifestdata" "github.com/hypermodeinc/modus/runtime/secrets" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" ) @@ -64,7 +65,7 @@ func InvokeModel(ctx context.Context, modelName string, input string) (string, e } func PostToModelEndpoint[TResult any](ctx context.Context, model *manifest.ModelInfo, payload any) (TResult, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() connInfo, err := httpclient.GetHttpConnectionInfo(model.Connection) diff --git a/runtime/pluginmanager/loader.go b/runtime/pluginmanager/loader.go index 0f82b4801..f7fad82c5 100644 --- a/runtime/pluginmanager/loader.go +++ b/runtime/pluginmanager/loader.go @@ -17,8 +17,8 @@ import ( "github.com/hypermodeinc/modus/runtime/db" "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/storage" - "github.com/hypermodeinc/modus/runtime/utils" "github.com/hypermodeinc/modus/runtime/wasmhost" ) @@ -26,9 +26,9 @@ func monitorPlugins(ctx context.Context) { loadPluginFile := func(fi storage.FileInfo) error { err := loadPlugin(ctx, fi.Name) if err != nil { - logger.Error(ctx, err). - Str("filename", fi.Name). - Msg("Failed to load plugin.") + const msg = "Failed to load plugin." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("filename", fi.Name)) + logger.Error(ctx, err).Str("filename", fi.Name).Msg(msg) } return err } @@ -39,9 +39,9 @@ func monitorPlugins(ctx context.Context) { sm.Removed = func(fi storage.FileInfo) error { err := unloadPlugin(ctx, fi.Name) if err != nil { - logger.Error(ctx, err). - Str("filename", fi.Name). - Msg("Failed to unload plugin.") + const msg = "Failed to unload plugin." + sentryutils.CaptureError(ctx, err, msg, sentryutils.WithData("filename", fi.Name)) + logger.Error(ctx, err).Str("filename", fi.Name).Msg(msg) } return err } @@ -49,7 +49,7 @@ func monitorPlugins(ctx context.Context) { } func loadPlugin(ctx context.Context, filename string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // Load the binary content of the plugin. @@ -68,9 +68,9 @@ func loadPlugin(ctx context.Context, filename string) error { // Get the metadata for the plugin. md, err := metadata.GetMetadataFromWasm(bytes) if err == metadata.ErrMetadataNotFound { - logger.Error(ctx). - Bool("user_visible", true). - Msg("Metadata not found. Please recompile using the latest version of the Modus SDK.") + const msg = "Metadata not found. Please recompile using the latest version of the Modus SDK." + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx).Bool("user_visible", true).Msg(msg) return err } else if err != nil { return err @@ -146,7 +146,7 @@ func logPluginLoaded(ctx context.Context, plugin *plugins.Plugin) { } func unloadPlugin(ctx context.Context, filename string) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() p, found := globalPluginRegistry.GetByFile(filename) diff --git a/runtime/plugins/plugins.go b/runtime/plugins/plugins.go index a57f8075b..933e60d40 100644 --- a/runtime/plugins/plugins.go +++ b/runtime/plugins/plugins.go @@ -17,6 +17,7 @@ import ( "github.com/hypermodeinc/modus/runtime/langsupport" "github.com/hypermodeinc/modus/runtime/languages" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/tetratelabs/wazero" @@ -34,7 +35,7 @@ type Plugin struct { } func NewPlugin(ctx context.Context, cm wazero.CompiledModule, filename string, md *metadata.Metadata) (*Plugin, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() language, err := languages.GetLanguageForSDK(md.SDK) diff --git a/runtime/secrets/kubernetes.go b/runtime/secrets/kubernetes.go index a84821a6d..d9eba9990 100644 --- a/runtime/secrets/kubernetes.go +++ b/runtime/secrets/kubernetes.go @@ -17,7 +17,7 @@ import ( "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,24 +37,32 @@ type kubernetesSecretsProvider struct { } func (sp *kubernetesSecretsProvider) initialize(ctx context.Context) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() fullName := app.Config().KubernetesSecretName() if fullName == "" { - logger.Fatal(ctx).Msg("A Kubernetes secret name is required when using Kubernetes secrets. Exiting.") + const msg = "A Kubernetes secret name is required when using Kubernetes secrets. Exiting." + sentryutils.CaptureError(ctx, nil, msg) + logger.Fatal(ctx).Msg(msg) } parts := strings.SplitN(fullName, "/", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - logger.Fatal(ctx).Str("input", fullName).Msg("Kubernetes secret name must be in the format /") + const msg = "Kubernetes secret name must be in the format /" + sentryutils.CaptureError(ctx, nil, msg, sentryutils.WithData("input", fullName)) + logger.Fatal(ctx).Str("input", fullName).Msg(msg) } sp.secretNamespace = parts[0] sp.secretName = parts[1] cli, cache, err := newK8sClientForSecret(sp.secretNamespace, sp.secretName) if err != nil { - logger.Fatal(ctx, err).Msg("Failed to initialize Kubernetes client.") + const msg = "Failed to initialize Kubernetes client." + sentryutils.CaptureError(ctx, err, msg, + sentryutils.WithData("namespace", sp.secretNamespace), + sentryutils.WithData("name", sp.secretName)) + logger.Fatal(ctx, err).Msg(msg) } sp.k8sClient = cli @@ -67,14 +75,22 @@ func (sp *kubernetesSecretsProvider) initialize(ctx context.Context) { // to call the API server everytime. go func() { if err := cache.Start(ctx); err != nil { - logger.Fatal(ctx, err).Msg("Failed to start Kubernetes client cache.") + const msg = "Failed to start Kubernetes client cache." + sentryutils.CaptureError(ctx, err, msg, + sentryutils.WithData("namespace", sp.secretNamespace), + sentryutils.WithData("name", sp.secretName)) + logger.Fatal(ctx, err).Msg(msg) } logger.Info(ctx).Msg("Kubernetes client cache stopped.") }() // Wait for initial cache sync if !cache.WaitForCacheSync(ctx) { - logger.Fatal(ctx).Msg("Failed to sync Kubernetes client cache.") + const msg = "Failed to sync Kubernetes client cache." + sentryutils.CaptureError(ctx, nil, msg, + sentryutils.WithData("namespace", sp.secretNamespace), + sentryutils.WithData("name", sp.secretName)) + logger.Fatal(ctx).Msg(msg) } logger.Info(ctx). diff --git a/runtime/secrets/secrets.go b/runtime/secrets/secrets.go index 5c13f6974..b6073442b 100644 --- a/runtime/secrets/secrets.go +++ b/runtime/secrets/secrets.go @@ -21,7 +21,7 @@ import ( "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" ) var provider secretsProvider @@ -85,7 +85,7 @@ func GetConnectionSecret(ctx context.Context, connection manifest.ConnectionInfo // ApplySecretsToHttpRequest evaluates the given request and replaces any placeholders // present in the query parameters and headers with their secret values for the given connection. func ApplySecretsToHttpRequest(ctx context.Context, connection *manifest.HTTPConnectionInfo, req *http.Request) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() // get secrets for the connection @@ -132,7 +132,7 @@ func ApplyAuthToLocalHypermodeModelRequest(ctx context.Context, connection manif // ApplySecretsToString evaluates the given string and replaces any placeholders // present in the string with their secret values for the given connection. func ApplySecretsToString(ctx context.Context, connection manifest.ConnectionInfo, str string) (string, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() secrets, err := GetConnectionSecrets(ctx, connection) diff --git a/runtime/sentryutils/sentry.go b/runtime/sentryutils/sentry.go new file mode 100644 index 000000000..b06e98aed --- /dev/null +++ b/runtime/sentryutils/sentry.go @@ -0,0 +1,284 @@ +/* + * Copyright 2025 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2025 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package sentryutils + +import ( + "context" + "os" + "path" + "runtime" + "strings" + "time" + + "github.com/hypermodeinc/modus/runtime/app" + "github.com/hypermodeinc/modus/runtime/utils" + + "github.com/getsentry/sentry-go" + "github.com/rs/zerolog/log" +) + +const max_breadcrumbs = 100 + +var rootSourcePath = func() string { + pc, filename, _, ok := runtime.Caller(0) + if !ok { + return "" + } + + callerName := runtime.FuncForPC(pc).Name() + depth := strings.Count(callerName, "/") + 1 + s := filename + for range depth { + s = path.Dir(s) + } + return s + "/" +}() + +var thisPackagePath = func() string { + pc, _, _, ok := runtime.Caller(0) + if !ok { + return "" + } + + callerName := runtime.FuncForPC(pc).Name() + i := max(strings.LastIndexByte(callerName, '/'), 0) + j := strings.IndexByte(callerName[i:], '.') + return callerName[0 : i+j] +}() + +func InitializeSentry() { + + // Don't initialize Sentry when running in debug mode. + if utils.DebugModeEnabled() { + return + } + + // ONLY report errors to Sentry when the SENTRY_DSN environment variable is set. + dsn := os.Getenv("SENTRY_DSN") + if dsn == "" { + return + } + + // Allow the Sentry environment to be overridden by the SENTRY_ENVIRONMENT environment variable, + // but default to the environment name from MODUS_ENV. + environment := os.Getenv("SENTRY_ENVIRONMENT") + if environment == "" { + environment = app.Config().Environment() + } + + // Allow the Sentry release to be overridden by the SENTRY_RELEASE environment variable, + // but default to the Modus version number. + release := os.Getenv("SENTRY_RELEASE") + if release == "" { + release = app.VersionNumber() + } + + err := sentry.Init(sentry.ClientOptions{ + Dsn: dsn, + Environment: environment, + Release: release, + BeforeSend: sentryBeforeSend, + BeforeSendTransaction: sentryBeforeSendTransaction, + + // Note - We use Prometheus for _metrics_ (see hypruntime/metrics package). + // However, we can still use Sentry for _tracing_ to allow us to improve performance of the Runtime. + // We should only trace code that we expect to run in a roughly consistent amount of time. + // For example, we should not trace a user's function execution, or the outer GraphQL request handling, + // because these can take an arbitrary amount of time depending on what the user's code does. + // Instead, we should trace the runtime's startup, storage, secrets retrieval, schema generation, etc. + // That way we can trace performance issues in the runtime itself, and let Sentry correlate them with + // any errors that may have occurred. + EnableTracing: true, + TracesSampleRate: utils.GetFloatFromEnv("SENTRY_TRACES_SAMPLE_RATE", 0.2), + }) + if err != nil { + log.Fatal().Err(err).Msg("Failed to initialize Sentry.") + } +} + +func CloseSentry() { + if err := recover(); err != nil { + hub := sentry.CurrentHub() + hub.Recover(err) + } + sentry.Flush(2 * time.Second) +} + +func NewSpanForCallingFunc(ctx context.Context) (*sentry.Span, context.Context) { + funcName := getFuncName(3) + return NewSpan(ctx, funcName) +} + +func NewSpanForCurrentFunc(ctx context.Context) (*sentry.Span, context.Context) { + funcName := getFuncName(2) + return NewSpan(ctx, funcName) +} + +func NewSpan(ctx context.Context, funcName string) (*sentry.Span, context.Context) { + if tx := sentry.TransactionFromContext(ctx); tx == nil { + tx = sentry.StartTransaction(ctx, funcName, sentry.WithOpName("function")) + return tx, tx.Context() + } + + span := sentry.StartSpan(ctx, "function") + span.Description = funcName + return span, span.Context() +} + +func getFuncName(skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + return "?" + } + + fn := runtime.FuncForPC(pc) + if fn == nil { + return "?" + } + + name := fn.Name() + return utils.TrimStringBefore(name, "/") +} + +func sentryBeforeSend(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + for _, e := range event.Exception { + if e.Stacktrace != nil { + frames := make([]sentry.Frame, 0, len(e.Stacktrace.Frames)) + + // Keep frames except those from the current package. + for f := range e.Stacktrace.Frames { + if e.Stacktrace.Frames[f].Module != thisPackagePath { + frames = append(frames, e.Stacktrace.Frames[f]) + } + } + + // Adjust frames to include relative paths to source files. + for i, f := range frames { + if f.Filename == "" && f.AbsPath != "" && strings.HasPrefix(f.AbsPath, rootSourcePath) { + f.Filename = f.AbsPath[len(rootSourcePath):] + frames[i] = f + } + } + + e.Stacktrace.Frames = frames + } + } + + addExtraData(event) + return event +} + +var ignoredTransactions = map[string]bool{ + "GET /health": true, + "GET /metrics": true, +} + +func sentryBeforeSendTransaction(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + if ignoredTransactions[event.Transaction] { + return nil + } + + addExtraData(event) + return event +} + +// Include any extra information that may be useful for debugging. +func addExtraData(event *sentry.Event) { + if ns, ok := app.KubernetesNamespace(); ok { + addExtraDataToEvent(event, "namespace", ns) + } +} + +func addExtraDataToEvent(event *sentry.Event, key string, value any) { + if event.Extra == nil { + event.Extra = make(map[string]any) + } + event.Extra[key] = value +} + +func Recover(ctx context.Context, r any) { + ActiveHub(ctx).RecoverWithContext(ctx, r) +} + +func CaptureError(ctx context.Context, err error, msg string, opts ...func(event *sentry.Event)) { + hub := ActiveHub(ctx) + var event *sentry.Event + if err == nil { + event = hub.Client().EventFromMessage(msg, sentry.LevelError) + } else { + event = hub.Client().EventFromException(err, sentry.LevelError) + event.Message = msg + } + for _, opt := range opts { + opt(event) + } + hub.CaptureEvent(event) +} + +func CaptureWarning(ctx context.Context, err error, msg string, opts ...func(event *sentry.Event)) { + hub := ActiveHub(ctx) + var event *sentry.Event + if err == nil { + event = hub.Client().EventFromMessage(msg, sentry.LevelWarning) + } else { + event = hub.Client().EventFromException(err, sentry.LevelWarning) + event.Message = msg + } + for _, opt := range opts { + opt(event) + } + hub.CaptureEvent(event) +} + +func WithData(key string, value any) func(event *sentry.Event) { + return func(event *sentry.Event) { + addExtraDataToEvent(event, key, value) + } +} + +func ActiveHub(ctx context.Context) *sentry.Hub { + if hub := sentry.GetHubFromContext(ctx); hub != nil { + return hub + } + + return sentry.CurrentHub() +} + +func NewScope(ctx context.Context) (scope *sentry.Scope, done func()) { + hub := ActiveHub(ctx) + scope = hub.PushScope() + return scope, func() { + hub.PopScope() + } +} + +func AddBreadcrumb(ctx context.Context, breadcrumb *sentry.Breadcrumb) { + ActiveHub(ctx).ConfigureScope(func(scope *sentry.Scope) { + scope.AddBreadcrumb(breadcrumb, max_breadcrumbs) + }) +} + +func AddTextBreadcrumb(ctx context.Context, message string) { + AddBreadcrumb(ctx, &sentry.Breadcrumb{ + Message: message, + Timestamp: time.Now().UTC(), + }) +} + +func AddBreadcrumbToScope(scope *sentry.Scope, breadcrumb *sentry.Breadcrumb) { + scope.AddBreadcrumb(breadcrumb, max_breadcrumbs) +} + +func AddTextBreadcrumbToScope(scope *sentry.Scope, message string) { + AddBreadcrumbToScope(scope, &sentry.Breadcrumb{ + Message: message, + Timestamp: time.Now().UTC(), + }) +} diff --git a/runtime/storage/awsstorage.go b/runtime/storage/awsstorage.go index 6e793fa70..52191d584 100644 --- a/runtime/storage/awsstorage.go +++ b/runtime/storage/awsstorage.go @@ -17,7 +17,7 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -31,7 +31,7 @@ type awsStorageProvider struct { } func (stg *awsStorageProvider) initialize(ctx context.Context) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() appConfig := app.Config() @@ -39,18 +39,24 @@ func (stg *awsStorageProvider) initialize(ctx context.Context) { stg.s3Path = appConfig.S3Path() if stg.s3Bucket == "" { - logger.Fatal(ctx).Msg("An S3 bucket is required when using AWS storage. Exiting.") + const msg = "An S3 bucket is required when using AWS storage. Exiting." + sentryutils.CaptureError(ctx, nil, msg) + logger.Fatal(ctx).Msg(msg) } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { - logger.Fatal(ctx, err).Msg("Failed to load AWS configuration. Exiting.") + const msg = "Failed to load AWS configuration. Exiting." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } client := sts.NewFromConfig(cfg) identity, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) if err != nil { - logger.Fatal(ctx, err).Msg("Failed to get AWS caller identity. Exiting.") + const msg = "Failed to get AWS caller identity. Exiting." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) } stg.s3Client = s3.NewFromConfig(cfg) diff --git a/runtime/storage/localstorage.go b/runtime/storage/localstorage.go index 2a766390b..b4039ff24 100644 --- a/runtime/storage/localstorage.go +++ b/runtime/storage/localstorage.go @@ -19,6 +19,7 @@ import ( "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/gofrs/flock" ) @@ -31,7 +32,9 @@ func (stg *localStorageProvider) initialize(ctx context.Context) { stg.appPath = app.Config().AppPath() if stg.appPath == "" { - logger.Fatal(ctx).Msg("The -appPath command line argument is required. Exiting.") + const msg = "The -appPath command line argument is required. Exiting." + sentryutils.CaptureError(ctx, nil, msg) + logger.Fatal(ctx).Msg(msg) } if _, err := os.Stat(stg.appPath); os.IsNotExist(err) { @@ -40,8 +43,10 @@ func (stg *localStorageProvider) initialize(ctx context.Context) { Msg("Creating app directory.") err := os.MkdirAll(stg.appPath, 0755) if err != nil { - logger.Fatal(ctx, err). - Msg("Failed to create local app directory. Exiting.") + const msg = "Failed to create local app directory. Exiting." + sentryutils.CaptureError(ctx, err, msg, + sentryutils.WithData("path", stg.appPath)) + logger.Fatal(ctx, err).Msg(msg) } } else { logger.Info(ctx). diff --git a/runtime/storage/storage.go b/runtime/storage/storage.go index 61a324649..080d05b90 100644 --- a/runtime/storage/storage.go +++ b/runtime/storage/storage.go @@ -14,7 +14,7 @@ import ( "time" "github.com/hypermodeinc/modus/runtime/app" - "github.com/hypermodeinc/modus/runtime/utils" + "github.com/hypermodeinc/modus/runtime/sentryutils" ) var provider storageProvider @@ -32,7 +32,7 @@ type FileInfo struct { } func Initialize(ctx context.Context) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() if app.Config().UseAwsStorage() { @@ -49,7 +49,7 @@ func ListFiles(ctx context.Context, patterns ...string) ([]FileInfo, error) { } func GetFileContents(ctx context.Context, name string) ([]byte, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() return provider.getFileContents(ctx, name) diff --git a/runtime/storage/storagemonitor.go b/runtime/storage/storagemonitor.go index 153ada933..64ff2d38e 100644 --- a/runtime/storage/storagemonitor.go +++ b/runtime/storage/storagemonitor.go @@ -11,10 +11,12 @@ package storage import ( "context" + "fmt" "time" "github.com/hypermodeinc/modus/runtime/app" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" ) type StorageMonitor struct { @@ -54,7 +56,9 @@ func (sm *StorageMonitor) Start(ctx context.Context) { if err != nil { // Don't stop watching. We'll just try again on the next cycle. if !loggedError { - logger.Error(ctx, err).Msgf("Failed to list %s files.", sm.patterns) + msg := fmt.Sprintf("Failed to list files matching patterns: %v", sm.patterns) + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) loggedError = true } continue diff --git a/runtime/utils/sentry.go b/runtime/utils/sentry.go deleted file mode 100644 index 59ceea8fe..000000000 --- a/runtime/utils/sentry.go +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2024 Hypermode Inc. - * Licensed under the terms of the Apache License, Version 2.0 - * See the LICENSE file that accompanied this code for further details. - * - * SPDX-FileCopyrightText: 2024 Hypermode Inc. - * SPDX-License-Identifier: Apache-2.0 - */ - -package utils - -import ( - "context" - "os" - "runtime" - "strings" - "time" - - "github.com/hypermodeinc/modus/runtime/app" - - "github.com/getsentry/sentry-go" - "github.com/rs/zerolog/log" -) - -var sentryInitialized bool - -var rootSourcePath = app.GetRootSourcePath() - -func InitializeSentry() { - - // Don't initialize Sentry when running in debug mode. - if DebugModeEnabled() { - return - } - - // ONLY report errors to Sentry when the SENTRY_DSN environment variable is set. - dsn := os.Getenv("SENTRY_DSN") - if dsn == "" { - return - } - - // Allow the Sentry environment to be overridden by the SENTRY_ENVIRONMENT environment variable, - // but default to the environment name from MODUS_ENV. - environment := os.Getenv("SENTRY_ENVIRONMENT") - if environment == "" { - environment = app.Config().Environment() - } - - // Allow the Sentry release to be overridden by the SENTRY_RELEASE environment variable, - // but default to the Modus version number. - release := os.Getenv("SENTRY_RELEASE") - if release == "" { - release = app.VersionNumber() - } - - err := sentry.Init(sentry.ClientOptions{ - Dsn: dsn, - Environment: environment, - Release: release, - BeforeSend: sentryBeforeSend, - BeforeSendTransaction: sentryBeforeSendTransaction, - - // Note - We use Prometheus for _metrics_ (see hypruntime/metrics package). - // However, we can still use Sentry for _tracing_ to allow us to improve performance of the Runtime. - // We should only trace code that we expect to run in a roughly consistent amount of time. - // For example, we should not trace a user's function execution, or the outer GraphQL request handling, - // because these can take an arbitrary amount of time depending on what the user's code does. - // Instead, we should trace the runtime's startup, storage, secrets retrieval, schema generation, etc. - // That way we can trace performance issues in the runtime itself, and let Sentry correlate them with - // any errors that may have occurred. - EnableTracing: true, - TracesSampleRate: GetFloatFromEnv("SENTRY_TRACES_SAMPLE_RATE", 0.2), - }) - if err != nil { - log.Fatal().Err(err).Msg("Failed to initialize Sentry.") - } - - sentryInitialized = true -} - -func FinalizeSentry() { - if sentryInitialized { - sentry.Recover() - sentry.Flush(5 * time.Second) - } -} - -func NewSentrySpanForCallingFunc(ctx context.Context) (*sentry.Span, context.Context) { - funcName := getFuncName(3) - return NewSentrySpan(ctx, funcName) -} - -func NewSentrySpanForCurrentFunc(ctx context.Context) (*sentry.Span, context.Context) { - funcName := getFuncName(2) - return NewSentrySpan(ctx, funcName) -} - -func NewSentrySpan(ctx context.Context, funcName string) (*sentry.Span, context.Context) { - if tx := sentry.TransactionFromContext(ctx); tx == nil { - tx = sentry.StartTransaction(ctx, funcName, sentry.WithOpName("function")) - return tx, tx.Context() - } - - span := sentry.StartSpan(ctx, "function") - span.Description = funcName - return span, span.Context() -} - -func getFuncName(skip int) string { - pc, _, _, ok := runtime.Caller(skip) - if !ok { - return "?" - } - - fn := runtime.FuncForPC(pc) - if fn == nil { - return "?" - } - - name := fn.Name() - return TrimStringBefore(name, "/") -} - -func sentryBeforeSend(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - - // Exclude user-visible errors from being reported to Sentry, because they are - // caused by user code, and thus are not actionable by us. - if event.Extra["user_visible"] == "true" { - return nil - } - - // Adjust the stack trace to show relative paths to the source code. - for _, e := range event.Exception { - st := *e.Stacktrace - for i, f := range st.Frames { - if f.Filename == "" && f.AbsPath != "" && strings.HasPrefix(f.AbsPath, rootSourcePath) { - f.Filename = f.AbsPath[len(rootSourcePath):] - st.Frames[i] = f - } - } - } - - sentryAddExtras(event) - return event -} - -func sentryBeforeSendTransaction(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - sentryAddExtras(event) - return event -} - -// Include any extra information that may be useful for debugging. -func sentryAddExtras(event *sentry.Event) { - if event.Extra == nil { - event.Extra = make(map[string]any) - } - - if ns, ok := app.KubernetesNamespace(); ok { - event.Extra["namespace"] = ns - } -} - -func CaptureError(ctx context.Context, err error) { - if !sentryInitialized || err == nil { - return - } - _ = sentryHub(ctx).CaptureException(err) -} - -func CaptureWarning(ctx context.Context, err error) { - if !sentryInitialized || err == nil { - return - } - - hub := sentryHub(ctx) - event := hub.Client().EventFromException(err, sentry.LevelWarning) - _ = hub.CaptureEvent(event) -} - -func sentryHub(ctx context.Context) *sentry.Hub { - if hub := sentry.GetHubFromContext(ctx); hub != nil { - return hub - } - - return sentry.CurrentHub() -} diff --git a/runtime/wasmhost/fncall.go b/runtime/wasmhost/fncall.go index d416ba0bc..c6b8e33ff 100644 --- a/runtime/wasmhost/fncall.go +++ b/runtime/wasmhost/fncall.go @@ -20,6 +20,7 @@ import ( "github.com/hypermodeinc/modus/runtime/functions" "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/metrics" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/rs/xid" @@ -77,7 +78,7 @@ func (host *wasmHost) CallFunctionByName(ctx context.Context, fnName string, par } func (host *wasmHost) CallFunction(ctx context.Context, fnInfo functions.FunctionInfo, parameters map[string]any) (ExecutionInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() plugin := fnInfo.Plugin() @@ -87,7 +88,6 @@ func (host *wasmHost) CallFunction(ctx context.Context, fnInfo functions.Functio mod, err := host.GetModuleInstance(ctx, plugin, buffers) if err != nil { - logger.Error(ctx, err).Msg("Error getting module instance.") return nil, err } defer mod.Close(ctx) @@ -96,7 +96,7 @@ func (host *wasmHost) CallFunction(ctx context.Context, fnInfo functions.Functio } func (host *wasmHost) CallFunctionInModule(ctx context.Context, mod wasm.Module, buffers utils.OutputBuffers, fnInfo functions.FunctionInfo, parameters map[string]any) (ExecutionInfo, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() executionId := xid.New().String() @@ -179,7 +179,7 @@ func (host *wasmHost) CallFunctionInModule(ctx context.Context, mod wasm.Module, fmt.Fprintln(os.Stderr, err) } // NOTE: Errors of this type should not be user-visible, as they were caused by some Runtime issue, not the user's code. - // This will also ensure the error is reported to Sentry. + sentryutils.CaptureError(ctx, err, "Error while executing function.", sentryutils.WithData("function", fnName)) logger.Error(ctx, err). Str("function", fnName). Dur("duration_ms", duration). diff --git a/runtime/wasmhost/hostfns.go b/runtime/wasmhost/hostfns.go index 6e8dacc01..4b25b19fd 100644 --- a/runtime/wasmhost/hostfns.go +++ b/runtime/wasmhost/hostfns.go @@ -20,6 +20,7 @@ import ( "github.com/hypermodeinc/modus/runtime/langsupport" "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" wasm "github.com/tetratelabs/wazero/api" @@ -202,15 +203,21 @@ func (host *wasmHost) newHostFunction(modName, funcName string, fn any, opts ... // Make the host function wrapper hf.function = wasm.GoModuleFunc(func(ctx context.Context, mod wasm.Module, stack []uint64) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() + scope, done := sentryutils.NewScope(ctx) + defer done() + sentryutils.AddTextBreadcrumbToScope(scope, "Starting host function: "+fullName) + defer sentryutils.AddTextBreadcrumbToScope(scope, "Finished host function: "+fullName) + // Log any panics that occur in the host function defer func() { if r := recover(); r != nil { + sentryutils.Recover(ctx, r) err := utils.ConvertToError(r) logger.Error(ctx, err).Str("host_function", fullName).Msg("Panic in host function.") - utils.CaptureError(ctx, err) + if utils.DebugModeEnabled() { debug.PrintStack() } @@ -220,14 +227,18 @@ func (host *wasmHost) newHostFunction(modName, funcName string, fn any, opts ... // Get the plugin of the function that invoked this host function plugin, ok := plugins.GetPluginFromContext(ctx) if !ok { - logger.Error(ctx).Str("host_function", fullName).Msg("Plugin not found in context.") + const msg = "Plugin not found in context." + sentryutils.CaptureError(ctx, nil, msg, sentryutils.WithData("host_function", fullName)) + logger.Error(ctx).Str("host_function", fullName).Msg(msg) return } // Get the execution plan for the host function plan, ok := plugin.ExecutionPlans[fullName] if !ok { - logger.Error(ctx).Str("host_function", fullName).Msg("Execution plan not found.") + const msg = "Execution plan not found." + sentryutils.CaptureError(ctx, nil, msg, sentryutils.WithData("host_function", fullName)) + logger.Error(ctx).Str("host_function", fullName).Msg(msg) return } @@ -250,7 +261,11 @@ func (host *wasmHost) newHostFunction(modName, funcName string, fn any, opts ... params = append(params, rvParam.Interface()) } if err := decodeParams(ctx, wa, plan, stack, params); err != nil { - logger.Error(ctx, err).Str("host_function", fullName).Any("data", params).Msg("Error decoding input parameters.") + const msg = "Error decoding input parameters." + sentryutils.CaptureError(ctx, err, msg, + sentryutils.WithData("host_function", fullName), + sentryutils.WithData("data", params)) + logger.Error(ctx, err).Str("host_function", fullName).Any("data", params).Msg(msg) return } @@ -318,7 +333,11 @@ func (host *wasmHost) newHostFunction(modName, funcName string, fn any, opts ... // Encode the results (if there are any) and write them to the stack if len(results) > 0 { if err := encodeResults(ctx, wa, plan, stack, results); err != nil { - logger.Error(ctx, err).Str("host_function", fullName).Any("data", results).Msg("Error encoding results.") + const msg = "Error encoding results." + sentryutils.CaptureError(ctx, err, msg, + sentryutils.WithData("host_function", fullName), + sentryutils.WithData("data", results)) + logger.Error(ctx, err).Str("host_function", fullName).Any("data", results).Msg(msg) } } }) @@ -327,7 +346,7 @@ func (host *wasmHost) newHostFunction(modName, funcName string, fn any, opts ... } func (host *wasmHost) instantiateHostFunctions(ctx context.Context) error { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() hostFnsByModule := make(map[string][]*hostFunction) diff --git a/runtime/wasmhost/initialization.go b/runtime/wasmhost/initialization.go index 672220ac6..0eeaa205a 100644 --- a/runtime/wasmhost/initialization.go +++ b/runtime/wasmhost/initialization.go @@ -13,13 +13,14 @@ import ( "context" "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/utils" "github.com/rs/zerolog" ) func InitWasmHost(ctx context.Context, registrations ...func(WasmHost) error) WasmHost { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() configureLogger() diff --git a/runtime/wasmhost/wasmhost.go b/runtime/wasmhost/wasmhost.go index 47da2f715..e68dba6ce 100644 --- a/runtime/wasmhost/wasmhost.go +++ b/runtime/wasmhost/wasmhost.go @@ -21,6 +21,7 @@ import ( "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/middleware" "github.com/hypermodeinc/modus/runtime/plugins" + "github.com/hypermodeinc/modus/runtime/sentryutils" "github.com/hypermodeinc/modus/runtime/timezones" "github.com/hypermodeinc/modus/runtime/utils" @@ -54,7 +55,9 @@ func NewWasmHost(ctx context.Context, registrations ...func(WasmHost) error) Was wasi.MustInstantiate(ctx, runtime) if err := instantiateEnvHostFunctions(ctx, runtime); err != nil { - logger.Fatal(ctx, err).Msg("Failed to instantiate env host functions.") + const msg = "Failed to instantiate env host functions." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) return nil } @@ -65,13 +68,17 @@ func NewWasmHost(ctx context.Context, registrations ...func(WasmHost) error) Was for _, reg := range registrations { if err := reg(host); err != nil { - logger.Fatal(ctx, err).Msg("Failed to apply a registration to the WASM host.") + const msg = "Failed to apply a registration to the WASM host." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) return nil } } if err := host.instantiateHostFunctions(ctx); err != nil { - logger.Fatal(ctx, err).Msg("Failed to instantiate host functions.") + const msg = "Failed to instantiate host functions." + sentryutils.CaptureError(ctx, err, msg) + logger.Fatal(ctx, err).Msg(msg) return nil } @@ -81,7 +88,9 @@ func NewWasmHost(ctx context.Context, registrations ...func(WasmHost) error) Was func GetWasmHost(ctx context.Context) WasmHost { host, ok := TryGetWasmHost(ctx) if !ok { - logger.Fatal(ctx).Msg("WASM Host not found in context.") + const msg = "WASM Host not found in context." + sentryutils.CaptureError(ctx, nil, msg) + logger.Fatal(ctx).Msg(msg) return nil } return host @@ -94,7 +103,9 @@ func TryGetWasmHost(ctx context.Context) (WasmHost, bool) { func (host *wasmHost) Close(ctx context.Context) { if err := host.runtime.Close(ctx); err != nil { - logger.Error(ctx, err).Msg("Failed to cleanly close the WASM runtime.") + const msg = "Failed to cleanly close the WASM runtime." + sentryutils.CaptureError(ctx, err, msg) + logger.Error(ctx, err).Msg(msg) } } @@ -108,7 +119,7 @@ func (host *wasmHost) GetFunctionRegistry() functions.FunctionRegistry { // Gets a module instance for the given plugin, used for a single invocation. func (host *wasmHost) GetModuleInstance(ctx context.Context, plugin *plugins.Plugin, buffers utils.OutputBuffers) (wasm.Module, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() cfg := getModuleConfig(ctx, buffers, plugin) @@ -121,7 +132,7 @@ func (host *wasmHost) GetModuleInstance(ctx context.Context, plugin *plugins.Plu } func (host *wasmHost) CompileModule(ctx context.Context, bytes []byte) (wazero.CompiledModule, error) { - span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) + span, ctx := sentryutils.NewSpanForCurrentFunc(ctx) defer span.Finish() cm, err := host.runtime.CompileModule(ctx, bytes)