Auto-Instrumentation
Auto-instrumentation removes per-function boilerplate by wrapping public callables as stages at the class, module, or import level.
@narrative_class
Wraps every public instance method of a class as a stage. The stage name is "ClassName.method_name".
from runtime_narrative import narrative_class, story
@narrative_class
class OrderService:
def validate(self, order: dict) -> None: # → stage "OrderService.validate"
if "amount" not in order:
raise ValueError("missing amount")
def charge(self, order: dict) -> str: # → stage "OrderService.charge"
return f"charged ${order['amount']:.2f}"
def fulfill(self, order: dict) -> str: # → stage "OrderService.fulfill"
return f"fulfilled order {order.get('id')}"
svc = OrderService()
with story("Process Order", total_stages=3):
svc.validate({"id": "ORD-1", "amount": 49.99})
receipt = svc.charge({"id": "ORD-1", "amount": 49.99})
svc.fulfill({"id": "ORD-1", "amount": 49.99})
What @narrative_class skips by default:
- Methods whose names start with
_(private/dunder) - Methods decorated with
@no_stage @classmethoddescriptors@staticmethoddescriptors@propertydescriptors- Methods inherited from parent classes (only
vars(cls)is inspected)
Opt in to classmethods and staticmethods:
@narrative_class(instrument_classmethods=True, instrument_staticmethods=True)
class Factory:
@classmethod
def create(cls, **kw): # → stage "Factory.create"
return cls(**kw)
@staticmethod
def validate_schema(data: dict): # → stage "Factory.validate_schema"
assert "type" in data
@narrative_stage
A per-function or per-method decorator for explicit stage naming. When name is omitted, the function name is title-cased. This decorator also works inside @narrative_class to override the default ClassName.method_name stage name — @narrative_class detects the _narrative_stage_name sentinel and skips re-wrapping to prevent double-instrumentation.
from runtime_narrative import narrative_stage, narrative_class, story
@narrative_stage("Validate Payment") # explicit custom name
def check_payment(card: str) -> bool:
return card.startswith("4")
@narrative_class
class PaymentService:
@narrative_stage("Charge Customer") # overrides default "PaymentService.charge"
def charge(self, amount: float) -> str:
return f"charged ${amount:.2f}"
def refund(self, amount: float) -> str: # default: "PaymentService.refund"
return f"refunded ${amount:.2f}"
with story("Checkout"):
check_payment("4111111111111111")
svc = PaymentService()
svc.charge(49.99)
@no_stage
An opt-out marker that excludes a method or function from all auto-instrumentation — @narrative_class, instrument_module, and auto_instrument all respect it.
from runtime_narrative import narrative_class, no_stage
@narrative_class
class ReportService:
def generate(self, data: list) -> str: # → stage "ReportService.generate"
return self._format(data)
@no_stage
def _format(self, data: list) -> str: # private; @no_stage for explicit clarity
return "\n".join(str(d) for d in data)
@no_stage
def debug_dump(self) -> None: # public but not a meaningful stage
print(repr(self))
@no_stage sets fn._narrative_skip = True. It does not change the function's behavior.
instrument_module
Instruments all public callables defined in an already-imported module in-place:
- Classes whose
__module__matches the module name get@narrative_classapplied - Top-level functions whose
__module__matches are wrapped directly - Symbols imported from other modules are not touched
import runtime_narrative
from runtime_narrative import story
import myapp.services as services
runtime_narrative.instrument_module(services) # call once after import
svc = services.CustomerService()
with story("Sync Customers", total_stages=3):
rows = svc.load("crm.csv")
svc.validate(rows)
count = svc.save(rows)
This is the right tool when retrofitting existing code you do not own or do not want to modify.
auto_instrument
Registers a sys.meta_path import hook that instruments every app module on import. Only modules whose source file starts with one of the app_roots paths are instrumented — stdlib and installed packages are unaffected.
import sys
import runtime_narrative
from runtime_narrative import story
from pathlib import Path
# Register the hook BEFORE importing any app modules
finder = runtime_narrative.auto_instrument(
app_roots=[str(Path(__file__).resolve().parent)]
)
# These modules are instrumented on the way in
import myapp.orders
import myapp.payments
with story("Process Order"):
svc = myapp.orders.OrderService()
svc.validate({"amount": 49.99})
svc.charge({"amount": 49.99})
# Remove the hook when you no longer need it
sys.meta_path.remove(finder)
auto_instrument defaults app_roots to the current working directory when called with no arguments:
runtime_narrative.auto_instrument() # instruments all modules under os.getcwd()
Next step
Continue to Failure Diagnostics to see how lean and rich modes capture the failure data these stages report.