Files
puter/src/backend/util/span.ts
T
Juan Fernando CastroandDaniel Salazar b54b76715a
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
feat(fs): finish a home rename that stopped partway (#4255)
* feat(fs): finish a home rename that stopped partway

FSEntryStore.rebaseUserSubtree runs renameUserHome's owner-scoped
cascade on its own (same subtree matching, same rebased-cache
invalidation) and also drops the home row's cached copies at either
path. countUserSubtree counts the owner's rows under a prefix with that
same matching, from the primary. renameUserHome now uses the shared
cascade.

* fix(telemetry): resolve the tracer per span so a test's provider is the one used

withSpan held a ProxyTracer built at module load, which caches the first
provider it delegates to. Test files sharing a worker then sent spans to a
provider an earlier file had already shut down.

* fix(fs): drop cached copies of only the rows a home rebase moves

The owner-scoped rewrite read its invalidation set from everything under the new prefix, which for a finishing rebase is the whole live home; it now reads the rows it is about to move.

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-20 00:59:40 -07:00

117 lines
3.9 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { type Attributes, SpanStatusCode, trace } from '@opentelemetry/api';
// Looked up per call: a cached ProxyTracer keeps the first provider it saw.
const tracer = () => trace.getTracer('puter-backend');
type AttrsOrFactory = Attributes | (() => Attributes);
/**
* Run `fn` inside an active span. Handles sync + async transparently, records
* exceptions, and always closes the span.
*/
export function withSpan<T>(
name: string,
attrs: AttrsOrFactory,
fn: () => T,
): T {
return tracer().startActiveSpan(name, (span) => {
try {
// Mirror the name into an attribute: X-Ray can only filter by
// annotations, and annotations come from attributes.
span.setAttribute('span_name', name);
const a = typeof attrs === 'function' ? attrs() : attrs;
if (a) span.setAttributes(a);
const result = fn();
if (result instanceof Promise) {
return result
.then(
(v) => {
span.setStatus({ code: SpanStatusCode.OK });
return v;
},
(err: unknown) => {
recordError(span, err);
throw err;
},
)
.finally(() => span.end()) as T;
}
span.setStatus({ code: SpanStatusCode.OK });
span.end();
return result;
} catch (err) {
recordError(span, err);
span.end();
throw err;
}
});
}
function recordError(
span: ReturnType<ReturnType<typeof tracer>['startSpan']>,
err: unknown,
) {
const e = err instanceof Error ? err : new Error(String(err));
span.recordException(e);
span.setStatus({ code: SpanStatusCode.ERROR, message: e.message });
}
/**
* Stage-3 method decorator: wrap the decorated method in a span. Usage: class
* Foo {
*
* @Span() // span name = "Foo.bar"
* async bar() { ... }
*
* @Span('custom.name') // span name = "custom.name"
* async baz() { ... }
*
* @Span('db.read', (query: string) => ({ 'db.statement': query }))
* async read(query: string) { ... } // attrs built from call args
* }
*/
export function Span(
name?: string,
attrs?: Attributes | ((...args: never[]) => Attributes),
) {
return function <This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
ctx: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Return
>,
) {
return function (this: This, ...args: Args): Return {
const spanName =
name ??
`${(this as { constructor?: { name?: string } })?.constructor?.name ?? 'fn'}.${String(ctx.name)}`;
const spanAttrs: AttrsOrFactory =
typeof attrs === 'function'
? () => attrs(...(args as unknown as never[]))
: (attrs ?? {});
return withSpan(spanName, spanAttrs, () =>
target.apply(this, args),
);
};
};
}