TriFrost traces middleware and route handlers out of the box — but what about everything else? In any real backend, there’s a whole ecosystem beyond routing: services, utilities, classes with methods that get reused across flows. Can we take our tracing a level deeper, without cluttering the code?
While building out the TriFrost website, I found myself writing this pattern multiple times:
class Releases {
async getReleases (ctx:Context) {
return ctx.logger.span('getReleases', async () => ctx.cache.wrap('releases', async () => {
...
}, {ttl: 3600}));
}
}It works, sure — but it’s boilerplate. I wanted something cleaner, something nearly invisible. This release brings exactly that.
Added
- feat:
@spandecorator for class methods - Wraps your method in a logger span automatically:
import {span} from '@trifrost/core';
class Releases {
@span()
async getReleases (ctx:Context) {
return ctx.cache.wrap('releases', async () => {
...
}, {ttl: 3600});
}
/* You can also pass a custom name if you prefer */
@span('release-loader')
async getRelease (ctx:Context) {
...
}
}
/**
* Pro-Tip: If you define a getter for logger on your class you don't
* even need to pass ctx into every function. The span decorator falls back to this.logger if not found on the first argument
*/
class Releases {
constructor (ctx:Context) {
this.ctx = ctx;
}
get logger () {
return ctx.logger;
}
@span()
async getReleases () {
... /* this.logger will be used here by span */
}
}- feat:
spanFnutility for standalone functions (sadly decorators don't work here just yet) which wraps a regular function or arrow function in a span, preserving this and ctx.logger when available:
import {spanFn} from '@trifrost/core';
const getReleases = spanFn('getReleases', async (ctx) => {
...
});
/* No name? No problem. spanFn will use the function name if defined (and fallback to anonymous as a last ditch effort) */
const getRelease = spanFn(async function getRelease (ctx:Context) => {
...
});Use them where it matters — tracing is now one line away.
You might ask yourself, why? Because you can't optimize what you don't measure, and through TriFrost we're dead-set on making measuring as easy as possible.
In the below image you'll see an example using SignOz in combination with the TriFrost Otel exporter (note the fetch trace as well, which is due to using ctx.fetch(...) which also spans internally: