Skip to content

Hooks interface & otel integration #663

@slinkydeveloper

Description

@slinkydeveloper

Introduce new hooks interface that will allow us to provide better integration with tracing, and other telemetry libs:

type AttemptResult = {
  type: "success"
} | {
  type: "retryableError"
  error: Error
} | {
  type: "terminalError"
  error: TerminalError
}

type Hooks = {
  // Set this hook to wrap the handler execution. Calling handlerRunner will cause the handler to run. 
  // Use it to propagate an async context storage.
  runInContext?: <T>(handlerRunner: () => Promise<T>) => Promise<T>;
  
  // Called when the attempt completes. Errors thrown inside this function are not propagated back to restate
  onAttemptEnd?: (result: AttemptResult) => void;
};

// If the hook provider fails, same rules as handler failures apply:
// * if fails with terminal error, invocation is terminated with terminal error
// * for other failures, it gets retried
type HooksProvider = (ctx: {
  serviceName: string;
  handlerName: string;
  key?: string;
  invocationId: string;
  request: Request;
}) => Hooks;

One can set these HooksProviders on a handler, on a service, or for the whole endpoint, and hooks will compose.

Tracing hooks provider example:

// Example provider: otel tracing
function otelTracing(tracer: Tracer): HooksProvider {
  return (ctx) => {
    // .... extract parent trace etc ...
    const span = tracer.startSpan(`${ctx.serviceName}/${ctx.handlerName}`, {"restate.invocation.id": ctx.invocationId});

    return {
      runInContext: (handlerRunner) =>
          otelContext.with(trace.setSpan(otelContext.active(), span), handlerRunner),
      onAttemptEnd: (result) => {
        if (result.type === "retryableError") {
          span.recordException(result.error);
          span.setAttribute("transient", true);
        } else if (result.type === "terminalError") {
          span.recordException(result.error);
        }
        span.end();
      },
    };
  };
}

const greeter = restate.service({
  name: "Greeter",
  handlers: {
    greet: async (ctx: restate.Context, name: string) => {
      // otel context — via otel API
      const span = trace.getActiveSpan();
      span?.addEvent("greeting_started");

      return `Hello, ${name}!`;
    },
  },
  options: {
    hooks: [otelTracing(tracer)]
  }
});

We could extend these hooks in future to do more things, like hooks on outgoing requests, etc.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions