Skip to content

Tag Nexus completion callback metrics with nexus_completion_source - #11927

Merged
tekkaya merged 1 commit into
mainfrom
gokhan/nexus-callback-completion-source-metric
Sep 9, 2026
Merged

tekkaya merged 1 commit into
mainfrom
gokhan/nexus-callback-completion-source-metric

Conversation

@tekkaya

@tekkaya tekkaya commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

  • Add an additional nexus_completion_source tag to currently emitted callback_outbound_requests and callback_outbound_latency metrics.
  • Improve callback completion logs with additional tags.

Why?

  • To distinguish nexus completion callback sources so that we can track what kind of durable execution is backing the nexus operation on the handler side.
  • To improve logging for better diagnose capabilities.

How did you test it?

  • built
  • run locally and tested manually
  • covered by existing tests
  • added new unit test(s)
  • added new functional test(s)

Potential risks

It is a tag addition to existing metrics, will work with DevProd related to the tag addition.

@tekkaya tekkaya changed the title Tag Nexus completion callback metrics with completion_source Tag Nexus completion callback metrics with nexus_completion_source Sep 3, 2026
@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch from ce0867f to 01f68a6 Compare September 3, 2026 21:56
@tekkaya
tekkaya marked this pull request as ready for review September 3, 2026 22:39
@tekkaya
tekkaya requested a review from a team September 3, 2026 22:39
@tekkaya
tekkaya requested review from a team as code owners September 3, 2026 22:39
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @tekkaya's task in 2m 45s —— View job


Reviewed 10 files, 2 findings.
· gokhan/nexus-callback-completion-source-metric

Comment thread chasm/lib/callback/component.go Outdated

type CompletionSource interface {
GetNexusCompletion(ctx chasm.Context, requestID string) (nexusrpc.CompleteOperationOptions, error)
GetNexusCompletionSourceVariant() string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — GetNexusCompletionSourceVariant uses a Get prefix and calls the value a "variant", which means something else in this codebase.

Repo Go conventions avoid the Get prefix on getters. GetNexusCompletion predates this and is constrained by the HSM CanGetNexusCompletion interface, but the new method is free to follow the convention.

"Variant" is also overloaded here: in the callback code it consistently refers to a proto oneof variant (callbackspb.Callback_Nexus_, "unprocessable callback variant: %v"). What this method returns is a metric tag value naming the component kind, which is what the interface name already says.

Same for the completionSourceVariant field on invocableOutbound (chasm/lib/callback/invocable_outbound.go:27) and the three implementations.

Suggestion: Rename to NexusCompletionSource() string (field: completionSource).

Suggested change
GetNexusCompletionSourceVariant() string
NexusCompletionSource() string

Comment thread common/metrics/tags.go
Comment on lines +511 to +513
if len(value) == 0 {
value = unknownValue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — The _unknown_ fallback is unreachable.

Every value reaching this tag is a compile-time constant: the three CompletionSource implementations return NexusCompletionSource{Workflow,WorkflowUpdate,Activity}TagValue, and nexus_invocation.go:77 passes the workflow constant literally. The neighboring tags in this file guard because they take dynamic or user-supplied values; this one can't be empty.

Suggestion: Drop the branch.

Suggested change
if len(value) == 0 {
value = unknownValue
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider adding a soft assert here because this really should never happen.

@tekkaya
tekkaya requested a review from bergundy September 3, 2026 22:42
@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch 2 times, most recently from d29b77e to 2af1f48 Compare September 4, 2026 01:28

@bergundy bergundy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you just double check the implication of adding a label to existing metrics?
I would also add what type of callback we are delivering: "nexus_handler", "nexus", "internal".

Comment thread chasm/lib/callback/component.go Outdated

type CompletionSource interface {
GetNexusCompletion(ctx chasm.Context, requestID string) (nexusrpc.CompleteOperationOptions, error)
GetNexusCompletionSourceVariant() SourceVariant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return this via the same GetNexusCompletion method. You can do that in a follow up PR though. I want to refactor this interface though, there's code that should run in the callback executor that right now every completion source has to implement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it in this PR 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'm a fan of adding another method to the CompletionSource interface, but also agree it should be refactoring since it's super clunky to use.

But rather than just returning the SourceVariant (or enumspb.ExecutionType), why not have it return the commonpb.Execution instead? (That is, the return value would also carry the execution_id and run_id values as well.)

That information can be obtained from the current chasm.Context. But (maybe in theory?) could differ. e.g. the Callback is CHASM execution X, and is delivering the Nexus result from CHASM execution Y? (Does that make sense @bergundy , or should this just be the execution type?)

The problem I see is that if we only return the execution type, we lose granularity. e.g. the callback would be associated with a Workflow instead of a Workflow Update...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout @chrsmith!

You would need the attached component name (update) and root component name (activity, workflow). Those should be obtainable via CHASM context APIs.
Technically there's nothing limiting the callback to be attached to a component nested two levels deep in the tree, so if you really wanted to future proof this, you would need an API that will give you the full name of each component in the path and the execution archetype. That doesn't exist today but I would not go this far as long as we can support those cases in the future while keeping whatever labels we emit today stable.

workflow.updates/update
workflow
activity
some-future-archetype.some-map/some-component.some-nested-map/some-nested-component

Comment thread common/metrics/tags.go
Comment on lines +511 to +513
if len(value) == 0 {
value = unknownValue
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider adding a soft assert here because this really should never happen.

@chrsmith chrsmith self-assigned this Sep 4, 2026
@tekkaya

tekkaya commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Can you just double check the implication of adding a label to existing metrics? I would also add what type of callback we are delivering: "nexus_handler", "nexus", "internal".

I did that last night and apparently for internal metrics new tags to existing metrics appear in Prometheus / Thanos (internal alerts, Grafana) right away but if we want them in Chronicle / ClickHouse (analytics) we need to ask DevProd to do that. Since the completion source variant and the callback variant you suggested are low cardinality (both has 3 values) we should be able use them for alerting right away. Do you think the dimension I suggest in this PR and the callback variant you suggested will be useful for analytics as well? In that case I'll reach out to dev prod related to it

@chrsmith chrsmith left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely something we want to do. But we might want to rework the way we are plumbing the data. (Using enumspb.ExecutinoType instead of a new CHASM-only enum, and thinking about how we'd update this when landing Worker Callbacks.)

I don't know how strongly I would push on these recommendations. (e.g. maybe adding another "callback-variant" tag wouldn't be a big deal?) So I'd defer to @bergundy.

Comment thread chasm/lib/callback/component.go Outdated
)

// SourceVariant names the kind of component that produced a Nexus operation completion callback.
type SourceVariant string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not introduce a callback.SourceVariant and try to rely on the existing enums we have today. There already is a commonpb.Execution proto and enumspb.ExecutionType enum that already provide this information.

i.e. we'd just have SourceExecutionType enumspb.ExecutionType instead.

https://github.com/temporalio/api/blob/main/temporal/api/common/v1/message.proto#L73
https://github.com/temporalio/api/blob/main/temporal/api/enums/v1/common.proto#L110

You'll notice that, sadly, EXECUTION_TYPE_NEXUS_OPERATION. But we should add that additional value. (In fact, it's part of the Worker Callbacks API PR that hasn't been merged yet. temporalio/api#863)

enum ExecutionType {
    EXECUTION_TYPE_UNSPECIFIED = 0;
    // A workflow execution archetype. 
    EXECUTION_TYPE_WORKFLOW = 1;
    // An activity execution archetype. This is reserved for standalone activities.
    EXECUTION_TYPE_ACTIVITY = 2;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we distinguish between workflow and workflow_update using ExecutionType?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrsmith I realized the API change https://github.com/temporalio/api/pull/863/changes#diff-de60adc512f72d87dc1e38c0f30b41a16dede9243dfaf6209b4af9b5447fa551R312 doesn't support expressing a callback attached to a workflow update.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bergundy , the plan was to use the component_id field. (Which admittedly isn't great, but will tide us over until we need another field like component_path.)

e.g. that PR, for the short term:

// For the short term.
Link_Callback{
   execution: {WORKFLOW, "wf-id-1", "run-id-1"},
   component_id: "wf-update-id-1",
}

// Later, adding another field when something other than workflow updates
// are setting the component_id.
Link_Callback{
   execution: {WORKFLOW, "wf-id-1", "run-id-1"},
   component_id: "wf-update-id-1",
   component_path: ["workflow", "update"] // Or something like that.
}

WDYT?

Comment thread chasm/lib/callback/component.go Outdated

type CompletionSource interface {
GetNexusCompletion(ctx chasm.Context, requestID string) (nexusrpc.CompleteOperationOptions, error)
GetNexusCompletionSourceVariant() SourceVariant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'm a fan of adding another method to the CompletionSource interface, but also agree it should be refactoring since it's super clunky to use.

But rather than just returning the SourceVariant (or enumspb.ExecutionType), why not have it return the commonpb.Execution instead? (That is, the return value would also carry the execution_id and run_id values as well.)

That information can be obtained from the current chasm.Context. But (maybe in theory?) could differ. e.g. the Callback is CHASM execution X, and is delivering the Nexus result from CHASM execution Y? (Does that make sense @bergundy , or should this just be the execution type?)

The problem I see is that if we only return the execution type, we lose granularity. e.g. the callback would be associated with a Workflow instead of a Workflow Update...

h.metricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, outcomeTag)
h.metricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, outcomeTag)
completionSourceTag := metrics.NexusCompletionSourceTag(string(n.completionSourceVariant))
h.metricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, outcomeTag, completionSourceTag)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that we'll want to add shortly, is the variant of the callback used. Today we only support the Nexus-variant callback. But (🤞) SOON, there will be a new NexusHandler-variant. Adding another tag seems kinda lame.

So could we combine it? e.g.

// Activity_Nexus, Workflow_Nexus. And later NexusOperation_NexusHandler?
completionInfo := `${completionSource}_${callbackVariant}`
``

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synced on this, decisions made:

  • will use component fully qualified name to distinguish the completion source
  • will add the callback variant as a separate tag

@bergundy

bergundy commented Sep 4, 2026

Copy link
Copy Markdown
Member

Can you just double check the implication of adding a label to existing metrics? I would also add what type of callback we are delivering: "nexus_handler", "nexus", "internal".

I did that last night and apparently for internal metrics new tags to existing metrics appear in Prometheus / Thanos (internal alerts, Grafana) right away but if we want them in Chronicle / ClickHouse (analytics) we need to ask DevProd to do that. Since the completion source variant and the callback variant you suggested are low cardinality (both has 3 values) we should be able use them for alerting right away. Do you think the dimension I suggest in this PR and the callback variant you suggested will be useful for analytics as well? In that case I'll reach out to dev prod related to it

That maps to my understanding as well. I think it's worth just communicating our plan to add these limited cardinality labels to the prometheus backend ATM.

@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch from 2af1f48 to 3e5f82d Compare September 5, 2026 06:03
@tekkaya
tekkaya requested a review from a team as a code owner September 5, 2026 06:03
@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch 2 times, most recently from dfaa39c to 5794a42 Compare September 5, 2026 06:15
@tekkaya
tekkaya requested review from bergundy and chrsmith September 6, 2026 04:32

@chrsmith chrsmith left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks good to me, but if possible (and doesn't make the code worse off), I'd like you to look at a couple of things.

Comment thread chasm/lib/callback/component.go Outdated

type CompletionSource interface {
// A callback's parent (completion source) is always a CHASM component, see [chasm.ParentPtr.TryGet].
chasm.Component

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a little dicy to embed the entire interface. Since it kinda conflates "CompletionSource" (which is an already confusing interface used for getting the result of a Nexus operation) with a prototypical CHASM component.

What do you think about having this be available via a getter, instead? e.g.

type CompletionSource interface {
    SourceComponent() chasm.Component
    GetNexusCompletion(...) (...)
}

Does that make the code cleaner? Or are we relying on it embedding the full interface for some reason?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting this behind a getter like SourceComponent() will still make it possible to access all accessible methods of the Component interface so I don't think we will gain from this change. One thing I will try to resolve the fqn from Node, instead of Component

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, having it behind SourceComponent() chasm.Component won't limit access in any way. But it will make any usages crystal clear.

e.g. without it, you could pass a CompletionSource to any location that would accept a chasm.Component, right? And that makes tracing things a little more difficult, as opposed to seeing all the places where the SourceComponent() method is called, and just looking there.

I wouldn't hold up this PR for that, it's just something I think we might want to consider since I'm guessing that we don't want to treat a CompletionSource as a regular CHASM component. And really just want a reliable way to extract metadata about where it came from.

type invocableOutbound struct {
callback *callbackspb.Callback_Nexus
completion nexusrpc.CompleteOperationOptions
completionSource string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could you add comment here explaining what completionSource means? As a string, it isn't clear what the role is. Ultimately, it's just the metric tag we emit right? So maybe renaming this to completionSourceTag or sourceTag might be clearer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that, also it is not just used as a metric tag, it is also used as a log tag as well

Comment thread chasm/tree.go Outdated

// componentFqn returns the fully qualified registered name of the given component, or "" if the
// component is not registered.
func (n *Node) componentFqn(component Component) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the component is not registered, wouldn't it make more sense to return ("", errors.New("component %T is not registered with CHASM", ...))? i.e. failing loudly when things aren't working, instead of silently emitting potentially bad data.

@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch from 5794a42 to 7116f3e Compare September 8, 2026 23:54
@tekkaya

tekkaya commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Updated ptal @chrsmith

@tekkaya
tekkaya requested a review from chrsmith September 8, 2026 23:55
Add a nexus_completion_source tag to callback_outbound_requests and
callback_outbound_latency, and a matching nexus-completion-source tag to
the Nexus completion callback logs, identifying which component produced
the completion: activity.activity, workflow.workflow or workflow.update.

Without it that telemetry is an undifferentiated union of workflow-backed
and activity-backed Nexus operation completions, so there is no way to
tell how many Nexus operations are backed by standalone activities.

The value is the completing component's fully qualified CHASM name rather
than a hand-declared string, so it cannot drift from registration. A new
ParentPtr.Fqn resolves it from the parent node's persisted type ID, which
needs no Context and does not deserialize the parent; the tree traversal
it shares with TryGet is factored out into parentNode. HSM callbacks are
always workflow-backed and report chasm.WorkflowArchetype directly.

The failure logs also gain the namespace, execution, and attempt context
they previously lacked. Log messages are unchanged so existing log-based
alerting keeps matching.
@tekkaya
tekkaya force-pushed the gokhan/nexus-callback-completion-source-metric branch from 7116f3e to 6deb9e0 Compare September 9, 2026 17:39

@chrsmith chrsmith left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An optional nitpick, but just go ahead and merge it so I can rebase and fix this in the pending PRs for worker callbacks.

// completionSourceTag is the fully qualified name of the CHASM component that produced this
// completion, e.g. "workflow.workflow" or "activity.activity".
completionSourceTag string
workflowID, runID string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: No need for another rev of the PR, but I'll rename workflowId to businessId. Since the callback might be attached to something other than a Workflow. (And "business ID" is the catch all instead of { activity, workflow, operation, ... } ID.)

@tekkaya
tekkaya enabled auto-merge (squash) September 9, 2026 18:23
h.metricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, outcomeMetricTag)
completionSourceMetricTag := metrics.NexusCompletionSourceTag(n.completionSourceTag)
h.metricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, outcomeMetricTag, completionSourceMetricTag)
h.metricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, outcomeMetricTag, completionSourceMetricTag)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to your changes, but probably more accurate to store time.Since(startTime right after L75.

Comment thread chasm/parent_pointer.go
return rc.fqType()
}

softassert.Fail(parent.logger, "parent component type is not registered with CHASM")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we panic instead if this is meant to never happen?

e.MetricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, statusCodeTag)
completionSourceTag := metrics.NexusCompletionSourceTag(chasm.WorkflowArchetype)
e.MetricsHandler.Counter(RequestCounter.Name()).Record(1, namespaceTag, destTag, statusCodeTag, completionSourceTag)
e.MetricsHandler.Timer(RequestLatencyHistogram.Name()).Record(time.Since(startTime), namespaceTag, destTag, statusCodeTag, completionSourceTag)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, unrelated but ideally, time.Since should be right after L74

@tekkaya
tekkaya merged commit 07050f8 into main Sep 9, 2026
90 of 94 checks passed
@tekkaya
tekkaya deleted the gokhan/nexus-callback-completion-source-metric branch September 9, 2026 21:34

@yycptt yycptt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor clarification questions. Nothing blocking given this is only for metric tags.

Comment thread chasm/parent_pointer.go
if !isT {
// nolint:forbidigo // Panic is intended here for framework error handling.
panic(serviceerror.NewInternalf("parent component value doesn't implement %s", reflect.TypeFor[T]().Name()))
if typeID := parent.serializedNode.GetMetadata().GetComponentAttributes().GetTypeId(); typeID != 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when will typeID be 0? is it for a newly created node and the code only performed syncStructure, but not the serialization part yet?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, TypeId is written on first serialization, in serializeComponentNode, so a node created in the current transaction reads 0 until CloseTransaction serializes it. typeID being 0 reachable in this scenario:

CloseTransaction runs executeImmediatePureTasks first, and syncSubComponents, which initializes ParentPtr.currentNode runs before those tasks, while closeTransactionSerializeNodes runs after. So an immediate pure task can hold an initialized ParentPtr whose parent has no TypeId yet. The fallback is trying to cover this case.

Comment thread chasm/parent_pointer.go
}
}
return vT, true
if rc, ok := parent.registry.componentFor(parent.value); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to call prepareComponentValue before access parent.value?

@tekkaya tekkaya Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do, currently this new Fqn method is only called from loadInvocationArgs method of the callback component and that method first makes a Get call to ParentPtr which makes the prepareComponentValue call. But you are right that Fqn can not rely on Get being called first, I'll move the prepareComponentValue to the new parentNode method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my earlier reply, we don't need it. prepareComponentValue resolves the type via registry.ComponentByID(TypeId), we do the same lookup in Fqn method already: if TypeId resolves, branch 1 has already returned the name; if it doesn't, prepareComponentValue fails too.

Will add a comment in the followup PR to clarify this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants