chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
357
third_party/pi/packages/telemetry/src/index.ts
vendored
Normal file
357
third_party/pi/packages/telemetry/src/index.ts
vendored
Normal file
@@ -0,0 +1,357 @@
|
||||
export type AttributeValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
|
||||
|
||||
export interface SpanAttributes {
|
||||
[name: string]: AttributeValue | undefined;
|
||||
}
|
||||
|
||||
export interface SpanOptions {
|
||||
name: string;
|
||||
attributes?: SpanAttributes;
|
||||
}
|
||||
|
||||
export type SpanStatus = { status: "ok" } | { status: "error"; error?: { name: string; message: string } };
|
||||
|
||||
export interface TelemetryContext {
|
||||
startSpan<T>(options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export interface TelemetrySpan extends TelemetryContext {
|
||||
addEvent(name: string, attributes?: SpanAttributes): void;
|
||||
setAttributes(attributes: SpanAttributes): void;
|
||||
setStatus(status: SpanStatus): void;
|
||||
}
|
||||
|
||||
export { NOOP_TELEMETRY_CONTEXT } from "./noop.ts";
|
||||
|
||||
export type TelemetryAttributeType = "string" | "number" | "boolean" | "string[]" | "number[]" | "boolean[]";
|
||||
|
||||
export interface TelemetryAttributeMetadata {
|
||||
description: string;
|
||||
sensitive?: boolean;
|
||||
cardinality?: "low" | "high";
|
||||
}
|
||||
|
||||
export type TelemetryAttributeDefinition = TelemetryAttributeMetadata &
|
||||
(
|
||||
| { type: "string"; values?: readonly string[]; examples?: readonly string[] }
|
||||
| { type: "number"; values?: readonly number[]; examples?: readonly number[] }
|
||||
| { type: "boolean"; values?: readonly boolean[]; examples?: readonly boolean[] }
|
||||
| { type: "string[]"; elementValues?: readonly string[]; examples?: readonly (readonly string[])[] }
|
||||
| { type: "number[]"; elementValues?: readonly number[]; examples?: readonly (readonly number[])[] }
|
||||
| { type: "boolean[]"; elementValues?: readonly boolean[]; examples?: readonly (readonly boolean[])[] }
|
||||
);
|
||||
|
||||
export type TelemetryStartAttributeDefinition = TelemetryAttributeDefinition & { required: boolean };
|
||||
export type TelemetryEventAttributeDefinition = TelemetryAttributeDefinition & { required: boolean };
|
||||
|
||||
export interface TelemetryEventDefinition {
|
||||
description: string;
|
||||
attributes: Record<string, TelemetryEventAttributeDefinition>;
|
||||
}
|
||||
|
||||
export type TelemetryParentDefinition =
|
||||
| { kind: "any" }
|
||||
| { kind: "root_or_external" }
|
||||
| { kind: "spans"; spans: readonly string[] };
|
||||
|
||||
export interface TelemetrySpanDefinition {
|
||||
description: string;
|
||||
parents: TelemetryParentDefinition;
|
||||
startAttributes: Record<string, TelemetryStartAttributeDefinition>;
|
||||
endAttributes: Record<string, TelemetryAttributeDefinition>;
|
||||
events?: Record<string, TelemetryEventDefinition>;
|
||||
status: { default: "ok"; errorWhen: string };
|
||||
}
|
||||
|
||||
export interface TelemetrySchemaDefinition {
|
||||
version: number;
|
||||
spans: Record<string, TelemetrySpanDefinition>;
|
||||
}
|
||||
|
||||
/** Typed identity helper for serializable telemetry schema data. */
|
||||
export function defineTelemetrySchema<const T extends TelemetrySchemaDefinition>(schema: T): T {
|
||||
return schema;
|
||||
}
|
||||
|
||||
type AttributeDefinitionValue<Definition extends TelemetryAttributeDefinition> = Definition extends {
|
||||
type: "string";
|
||||
values: readonly (infer Value extends string)[];
|
||||
}
|
||||
? Value
|
||||
: Definition extends { type: "string" }
|
||||
? string
|
||||
: Definition extends { type: "number"; values: readonly (infer Value extends number)[] }
|
||||
? Value
|
||||
: Definition extends { type: "number" }
|
||||
? number
|
||||
: Definition extends { type: "boolean"; values: readonly (infer Value extends boolean)[] }
|
||||
? Value
|
||||
: Definition extends { type: "boolean" }
|
||||
? boolean
|
||||
: Definition extends {
|
||||
type: "string[]";
|
||||
elementValues: readonly (infer Value extends string)[];
|
||||
}
|
||||
? readonly Value[]
|
||||
: Definition extends { type: "string[]" }
|
||||
? readonly string[]
|
||||
: Definition extends {
|
||||
type: "number[]";
|
||||
elementValues: readonly (infer Value extends number)[];
|
||||
}
|
||||
? readonly Value[]
|
||||
: Definition extends { type: "number[]" }
|
||||
? readonly number[]
|
||||
: Definition extends {
|
||||
type: "boolean[]";
|
||||
elementValues: readonly (infer Value extends boolean)[];
|
||||
}
|
||||
? readonly Value[]
|
||||
: readonly boolean[];
|
||||
|
||||
type RequiredAttributeNames<
|
||||
Definitions extends Record<string, TelemetryStartAttributeDefinition | TelemetryEventAttributeDefinition>,
|
||||
> = {
|
||||
[Name in keyof Definitions]-?: Definitions[Name]["required"] extends true ? Name : never;
|
||||
}[keyof Definitions];
|
||||
|
||||
type OptionalAttributeNames<
|
||||
Definitions extends Record<string, TelemetryStartAttributeDefinition | TelemetryEventAttributeDefinition>,
|
||||
> = Exclude<keyof Definitions, RequiredAttributeNames<Definitions>>;
|
||||
|
||||
export type InferRequiredAndOptionalAttributes<
|
||||
Definitions extends Record<string, TelemetryStartAttributeDefinition | TelemetryEventAttributeDefinition>,
|
||||
> = keyof Definitions extends never
|
||||
? Record<string, never>
|
||||
: {
|
||||
[Name in RequiredAttributeNames<Definitions>]: AttributeDefinitionValue<Definitions[Name]>;
|
||||
} & {
|
||||
[Name in OptionalAttributeNames<Definitions>]?: AttributeDefinitionValue<Definitions[Name]>;
|
||||
};
|
||||
|
||||
export type InferStartAttributes<Definitions extends Record<string, TelemetryStartAttributeDefinition>> =
|
||||
InferRequiredAndOptionalAttributes<Definitions>;
|
||||
|
||||
export type InferOptionalAttributes<Definitions extends Record<string, TelemetryAttributeDefinition>> =
|
||||
keyof Definitions extends never
|
||||
? Record<string, never>
|
||||
: { [Name in keyof Definitions]?: AttributeDefinitionValue<Definitions[Name]> };
|
||||
|
||||
export type ExactTelemetryAttributes<Expected, Actual extends Expected> = Actual &
|
||||
Record<Exclude<keyof Actual, keyof Expected>, never>;
|
||||
|
||||
export type InferEventAttributes<Definitions extends Record<string, TelemetryEventAttributeDefinition>> =
|
||||
InferRequiredAndOptionalAttributes<Definitions>;
|
||||
|
||||
export type TelemetrySchemaSpanName<Schema extends TelemetrySchemaDefinition> = keyof Schema["spans"] & string;
|
||||
|
||||
type SchemaSpan<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = Schema["spans"][Name];
|
||||
|
||||
export type TelemetrySchemaSpanStartAttributes<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = SchemaSpan<Schema, Name>["startAttributes"] extends infer Definitions extends Record<
|
||||
string,
|
||||
TelemetryStartAttributeDefinition
|
||||
>
|
||||
? InferStartAttributes<Definitions>
|
||||
: never;
|
||||
|
||||
export type TelemetrySchemaSpanEndAttributes<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = SchemaSpan<Schema, Name>["endAttributes"] extends infer Definitions extends Record<
|
||||
string,
|
||||
TelemetryAttributeDefinition
|
||||
>
|
||||
? InferOptionalAttributes<Definitions>
|
||||
: never;
|
||||
|
||||
type SchemaSpanEvents<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = SchemaSpan<Schema, Name> extends { events: infer Events extends Record<string, TelemetryEventDefinition> }
|
||||
? Events
|
||||
: Record<never, never>;
|
||||
|
||||
export type TelemetrySchemaSpanEventName<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = keyof SchemaSpanEvents<Schema, Name> & string;
|
||||
|
||||
type SchemaSpanEvent<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
EventName extends TelemetrySchemaSpanEventName<Schema, Name>,
|
||||
> = SchemaSpanEvents<Schema, Name> extends infer Events
|
||||
? EventName extends keyof Events
|
||||
? Events[EventName]
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TelemetrySchemaSpanEventAttributes<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
EventName extends TelemetrySchemaSpanEventName<Schema, Name>,
|
||||
> = SchemaSpanEvent<Schema, Name, EventName> extends {
|
||||
attributes: infer Definitions extends Record<string, TelemetryEventAttributeDefinition>;
|
||||
}
|
||||
? InferEventAttributes<Definitions>
|
||||
: never;
|
||||
|
||||
type SchemaSpanEventAttributeDefinitions<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
EventName extends TelemetrySchemaSpanEventName<Schema, Name>,
|
||||
> = SchemaSpanEvent<Schema, Name, EventName> extends {
|
||||
attributes: infer Definitions extends Record<string, TelemetryEventAttributeDefinition>;
|
||||
}
|
||||
? Definitions
|
||||
: Record<never, never>;
|
||||
|
||||
type EventArguments<
|
||||
Definitions extends Record<string, TelemetryEventAttributeDefinition>,
|
||||
Attributes extends InferEventAttributes<Definitions>,
|
||||
> = [RequiredAttributeNames<Definitions>] extends [never]
|
||||
? [attributes?: ExactTelemetryAttributes<InferEventAttributes<Definitions>, Attributes>]
|
||||
: [attributes: ExactTelemetryAttributes<InferEventAttributes<Definitions>, Attributes>];
|
||||
|
||||
export type SchemaTelemetrySpan<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends TelemetrySchemaSpanName<Schema>,
|
||||
> = Omit<TelemetrySpan, "addEvent" | "setAttributes"> & {
|
||||
addEvent<
|
||||
EventName extends TelemetrySchemaSpanEventName<Schema, Name>,
|
||||
const Attributes extends InferEventAttributes<
|
||||
SchemaSpanEventAttributeDefinitions<Schema, Name, EventName>
|
||||
> = InferEventAttributes<SchemaSpanEventAttributeDefinitions<Schema, Name, EventName>>,
|
||||
>(
|
||||
name: EventName,
|
||||
...args: EventArguments<SchemaSpanEventAttributeDefinitions<Schema, Name, EventName>, Attributes>
|
||||
): void;
|
||||
setAttributes<const Attributes extends TelemetrySchemaSpanEndAttributes<Schema, Name>>(
|
||||
attributes: ExactTelemetryAttributes<TelemetrySchemaSpanEndAttributes<Schema, Name>, Attributes>,
|
||||
): void;
|
||||
};
|
||||
|
||||
export type TelemetrySchemaSpanUnion<Schema extends TelemetrySchemaDefinition> = {
|
||||
[Name in TelemetrySchemaSpanName<Schema>]: {
|
||||
name: Name;
|
||||
startAttributes: TelemetrySchemaSpanStartAttributes<Schema, Name>;
|
||||
endAttributes: TelemetrySchemaSpanEndAttributes<Schema, Name>;
|
||||
events: {
|
||||
[EventName in TelemetrySchemaSpanEventName<Schema, Name>]: TelemetrySchemaSpanEventAttributes<
|
||||
Schema,
|
||||
Name,
|
||||
EventName
|
||||
>;
|
||||
};
|
||||
};
|
||||
}[TelemetrySchemaSpanName<Schema>];
|
||||
|
||||
type TelemetrySchemaTuple = readonly [TelemetrySchemaDefinition, ...TelemetrySchemaDefinition[]];
|
||||
|
||||
type SpanNameInSchema<Schema extends TelemetrySchemaDefinition> = Schema extends TelemetrySchemaDefinition
|
||||
? TelemetrySchemaSpanName<Schema>
|
||||
: never;
|
||||
|
||||
type SpanNameInSchemas<Schemas extends TelemetrySchemaTuple> = SpanNameInSchema<Schemas[number]>;
|
||||
|
||||
type SpanStartAttributesInSchema<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends string,
|
||||
> = Schema extends TelemetrySchemaDefinition
|
||||
? Name extends TelemetrySchemaSpanName<Schema>
|
||||
? TelemetrySchemaSpanStartAttributes<Schema, Name>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type SpanInSchema<
|
||||
Schema extends TelemetrySchemaDefinition,
|
||||
Name extends string,
|
||||
> = Schema extends TelemetrySchemaDefinition
|
||||
? Name extends TelemetrySchemaSpanName<Schema>
|
||||
? SchemaTelemetrySpan<Schema, Name>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type DuplicateTelemetrySpanNames<
|
||||
Schemas extends readonly TelemetrySchemaDefinition[],
|
||||
Seen extends string = never,
|
||||
> = Schemas extends readonly [
|
||||
infer Schema extends TelemetrySchemaDefinition,
|
||||
...infer Rest extends readonly TelemetrySchemaDefinition[],
|
||||
]
|
||||
?
|
||||
| Extract<TelemetrySchemaSpanName<Schema>, Seen>
|
||||
| DuplicateTelemetrySpanNames<Rest, Seen | TelemetrySchemaSpanName<Schema>>
|
||||
: never;
|
||||
|
||||
type UniqueTelemetrySchemas<Schemas extends TelemetrySchemaTuple> = [DuplicateTelemetrySpanNames<Schemas>] extends [
|
||||
never,
|
||||
]
|
||||
? unknown
|
||||
: { readonly "duplicate telemetry span names": DuplicateTelemetrySpanNames<Schemas> };
|
||||
|
||||
type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (
|
||||
value: infer Intersection,
|
||||
) => void
|
||||
? Intersection
|
||||
: never;
|
||||
|
||||
type TypedSpanStarterForName<Schemas extends TelemetrySchemaTuple, Name extends SpanNameInSchemas<Schemas>> = <
|
||||
const Attributes extends SpanStartAttributesInSchema<Schemas[number], Name>,
|
||||
Result,
|
||||
>(
|
||||
name: Name,
|
||||
attributes: ExactTelemetryAttributes<SpanStartAttributesInSchema<Schemas[number], Name>, Attributes>,
|
||||
callback: (
|
||||
span: SpanInSchema<Schemas[number], Name>,
|
||||
startChildSpan: TypedSpanStarter<Schemas>,
|
||||
) => Result | Promise<Result>,
|
||||
) => Promise<Result>;
|
||||
|
||||
/** A per-span overload set bound to one explicit parent context and one or more schemas. */
|
||||
export type TypedSpanStarter<Schemas extends TelemetrySchemaTuple> = UnionToIntersection<
|
||||
{
|
||||
[Name in SpanNameInSchemas<Schemas>]: TypedSpanStarterForName<Schemas, Name>;
|
||||
}[SpanNameInSchemas<Schemas>]
|
||||
>;
|
||||
|
||||
function bindTypedSpanStarter<Schemas extends TelemetrySchemaTuple>(
|
||||
telemetryContext: TelemetryContext,
|
||||
): TypedSpanStarter<Schemas> {
|
||||
const startSpan = (
|
||||
name: SpanNameInSchemas<Schemas>,
|
||||
attributes: SpanAttributes,
|
||||
callback: (
|
||||
span: SpanInSchema<Schemas[number], SpanNameInSchemas<Schemas>>,
|
||||
startChildSpan: TypedSpanStarter<Schemas>,
|
||||
) => unknown,
|
||||
): Promise<unknown> =>
|
||||
telemetryContext.startSpan({ name, attributes }, (span) =>
|
||||
callback(
|
||||
span as SpanInSchema<Schemas[number], SpanNameInSchemas<Schemas>>,
|
||||
bindTypedSpanStarter<Schemas>(span),
|
||||
),
|
||||
);
|
||||
|
||||
return startSpan as TypedSpanStarter<Schemas>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an explicit parent context to the combined span vocabulary of one or more schemas.
|
||||
* Schema values are used only for type inference; no runtime schema validation is performed.
|
||||
*/
|
||||
export function createTypedSpanStarter<const Schemas extends TelemetrySchemaTuple>(
|
||||
telemetryContext: TelemetryContext,
|
||||
_schemas: Schemas & UniqueTelemetrySchemas<Schemas>,
|
||||
): TypedSpanStarter<Schemas> {
|
||||
return bindTypedSpanStarter<Schemas>(telemetryContext);
|
||||
}
|
||||
|
||||
export type { RecordedTelemetryEvent, RecordedTelemetrySpan } from "./memory.ts";
|
||||
export { InMemoryTelemetryContext } from "./memory.ts";
|
||||
219
third_party/pi/packages/telemetry/src/memory.ts
vendored
Normal file
219
third_party/pi/packages/telemetry/src/memory.ts
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
import type {
|
||||
AttributeValue,
|
||||
SpanAttributes,
|
||||
SpanOptions,
|
||||
SpanStatus,
|
||||
TelemetryContext,
|
||||
TelemetrySpan,
|
||||
} from "./index.ts";
|
||||
import { NOOP_TELEMETRY_CONTEXT } from "./noop.ts";
|
||||
|
||||
export interface RecordedTelemetryEvent {
|
||||
readonly name: string;
|
||||
readonly attributes: Readonly<SpanAttributes>;
|
||||
}
|
||||
|
||||
export interface RecordedTelemetrySpan {
|
||||
readonly id: number;
|
||||
readonly parentId: number | null;
|
||||
readonly name: string;
|
||||
readonly attributes: Readonly<SpanAttributes>;
|
||||
readonly events: readonly RecordedTelemetryEvent[];
|
||||
readonly status: SpanStatus;
|
||||
readonly settled: boolean;
|
||||
readonly endSequence?: number;
|
||||
}
|
||||
|
||||
interface MutableRecordedTelemetryEvent {
|
||||
name: string;
|
||||
attributes: SpanAttributes;
|
||||
}
|
||||
|
||||
interface MutableRecordedTelemetrySpan {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
name: string;
|
||||
attributes: SpanAttributes;
|
||||
events: MutableRecordedTelemetryEvent[];
|
||||
status: SpanStatus;
|
||||
explicitStatus: boolean;
|
||||
settled: boolean;
|
||||
endSequence?: number;
|
||||
}
|
||||
|
||||
interface InMemoryTelemetryState {
|
||||
spans: MutableRecordedTelemetrySpan[];
|
||||
nextSpanId: number;
|
||||
nextEndSequence: number;
|
||||
}
|
||||
|
||||
function copyAttributeValue(value: AttributeValue): AttributeValue {
|
||||
return Array.isArray(value) ? ([...value] as AttributeValue) : value;
|
||||
}
|
||||
|
||||
function copyAttributes(attributes?: SpanAttributes): SpanAttributes {
|
||||
const copy: SpanAttributes = {};
|
||||
if (!attributes) return copy;
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
if (value !== undefined) copy[name] = copyAttributeValue(value);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function mergeAttributes(current: SpanAttributes, attributes: SpanAttributes): SpanAttributes {
|
||||
const merged = copyAttributes(current);
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
if (value !== undefined) merged[name] = copyAttributeValue(value);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function copyStatus(status: SpanStatus): SpanStatus {
|
||||
if (status.status === "ok") return { status: "ok" };
|
||||
return status.error
|
||||
? { status: "error", error: { name: status.error.name, message: status.error.message } }
|
||||
: { status: "error" };
|
||||
}
|
||||
|
||||
function automaticErrorStatus(error: unknown): SpanStatus {
|
||||
try {
|
||||
if (error instanceof Error) {
|
||||
return { status: "error", error: { name: error.name, message: error.message } };
|
||||
}
|
||||
} catch {
|
||||
// Error inspection is passive. Fall through to an error status without details.
|
||||
}
|
||||
return { status: "error" };
|
||||
}
|
||||
|
||||
function settleSpan(
|
||||
state: InMemoryTelemetryState,
|
||||
span: MutableRecordedTelemetrySpan,
|
||||
failed: boolean,
|
||||
error?: unknown,
|
||||
): void {
|
||||
if (span.settled) return;
|
||||
if (failed && !span.explicitStatus) span.status = automaticErrorStatus(error);
|
||||
span.settled = true;
|
||||
span.endSequence = state.nextEndSequence++;
|
||||
}
|
||||
|
||||
function createSpan(
|
||||
state: InMemoryTelemetryState,
|
||||
parent: MutableRecordedTelemetrySpan | undefined,
|
||||
options: SpanOptions,
|
||||
): MutableRecordedTelemetrySpan {
|
||||
const name = options.name;
|
||||
const attributes = copyAttributes(options.attributes);
|
||||
return {
|
||||
id: state.nextSpanId++,
|
||||
parentId: parent?.id ?? null,
|
||||
name,
|
||||
attributes,
|
||||
events: [],
|
||||
status: { status: "ok" },
|
||||
explicitStatus: false,
|
||||
settled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function startInMemorySpan<T>(
|
||||
state: InMemoryTelemetryState,
|
||||
parent: MutableRecordedTelemetrySpan | undefined,
|
||||
options: SpanOptions,
|
||||
callback: (span: TelemetrySpan) => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
if (parent?.settled) return NOOP_TELEMETRY_CONTEXT.startSpan(options, callback);
|
||||
|
||||
let recordedSpan: MutableRecordedTelemetrySpan;
|
||||
try {
|
||||
recordedSpan = createSpan(state, parent, options);
|
||||
state.spans.push(recordedSpan);
|
||||
} catch {
|
||||
return NOOP_TELEMETRY_CONTEXT.startSpan(options, callback);
|
||||
}
|
||||
|
||||
const span: TelemetrySpan = {
|
||||
startSpan: <Result>(
|
||||
childOptions: SpanOptions,
|
||||
childCallback: (child: TelemetrySpan) => Result | Promise<Result>,
|
||||
) => startInMemorySpan(state, recordedSpan, childOptions, childCallback),
|
||||
addEvent(name, attributes) {
|
||||
if (recordedSpan.settled) return;
|
||||
try {
|
||||
recordedSpan.events.push({ name, attributes: copyAttributes(attributes) });
|
||||
} catch {
|
||||
// Recording is passive. Ignore malformed or unreadable telemetry payloads.
|
||||
}
|
||||
},
|
||||
setAttributes(attributes) {
|
||||
if (recordedSpan.settled) return;
|
||||
try {
|
||||
recordedSpan.attributes = mergeAttributes(recordedSpan.attributes, attributes);
|
||||
} catch {
|
||||
// Recording is passive. Ignore malformed or unreadable telemetry payloads.
|
||||
}
|
||||
},
|
||||
setStatus(status) {
|
||||
if (recordedSpan.settled) return;
|
||||
try {
|
||||
recordedSpan.status = copyStatus(status);
|
||||
recordedSpan.explicitStatus = true;
|
||||
} catch {
|
||||
// Recording is passive. Ignore malformed or unreadable telemetry payloads.
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let result: T | Promise<T>;
|
||||
try {
|
||||
result = callback(span);
|
||||
} catch (error) {
|
||||
settleSpan(state, recordedSpan, true, error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return Promise.resolve(result).then(
|
||||
(value) => {
|
||||
settleSpan(state, recordedSpan, false);
|
||||
return value;
|
||||
},
|
||||
(error: unknown) => {
|
||||
settleSpan(state, recordedSpan, true, error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend-neutral reference implementation that records spans in process memory.
|
||||
* Create a fresh instance to isolate tests or independent recording scopes.
|
||||
*/
|
||||
export class InMemoryTelemetryContext implements TelemetryContext {
|
||||
private readonly state: InMemoryTelemetryState = {
|
||||
spans: [],
|
||||
nextSpanId: 1,
|
||||
nextEndSequence: 1,
|
||||
};
|
||||
|
||||
startSpan<T>(options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T> {
|
||||
return startInMemorySpan(this.state, undefined, options, callback);
|
||||
}
|
||||
|
||||
/** Returns detached snapshots in span-start order. */
|
||||
getSpans(): readonly RecordedTelemetrySpan[] {
|
||||
return this.state.spans.map((span) => ({
|
||||
id: span.id,
|
||||
parentId: span.parentId,
|
||||
name: span.name,
|
||||
attributes: copyAttributes(span.attributes),
|
||||
events: span.events.map((event) => ({
|
||||
name: event.name,
|
||||
attributes: copyAttributes(event.attributes),
|
||||
})),
|
||||
status: copyStatus(span.status),
|
||||
settled: span.settled,
|
||||
...(span.endSequence === undefined ? {} : { endSequence: span.endSequence }),
|
||||
}));
|
||||
}
|
||||
}
|
||||
20
third_party/pi/packages/telemetry/src/noop.ts
vendored
Normal file
20
third_party/pi/packages/telemetry/src/noop.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SpanOptions, TelemetryContext, TelemetrySpan } from "./index.ts";
|
||||
|
||||
function startNoopSpan<T>(_options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T> {
|
||||
try {
|
||||
return Promise.resolve(callback(noopTelemetrySpan));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const noopTelemetrySpan: TelemetrySpan = {
|
||||
startSpan: startNoopSpan,
|
||||
addEvent: () => {},
|
||||
setAttributes: () => {},
|
||||
setStatus: () => {},
|
||||
};
|
||||
Object.freeze(noopTelemetrySpan);
|
||||
|
||||
/** Shared telemetry context used when an application does not provide one. */
|
||||
export const NOOP_TELEMETRY_CONTEXT: TelemetryContext = noopTelemetrySpan;
|
||||
315
third_party/pi/packages/telemetry/src/testing/conformance.ts
vendored
Normal file
315
third_party/pi/packages/telemetry/src/testing/conformance.ts
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||
import { deepStrictEqual, doesNotThrow, fail, ok, strictEqual } from "node:assert/strict";
|
||||
import type { SpanAttributes, SpanOptions, SpanStatus, TelemetrySpan } from "../index.ts";
|
||||
import type { RecordedTelemetrySpan } from "../memory.ts";
|
||||
import type {
|
||||
TelemetryAdapterConformanceCase,
|
||||
TelemetryAdapterFixture,
|
||||
TelemetryAdapterFixtureFactory,
|
||||
} from "./types.ts";
|
||||
|
||||
type ConformanceTest = (fixture: TelemetryAdapterFixture) => Promise<void>;
|
||||
|
||||
function createCase(
|
||||
factory: TelemetryAdapterFixtureFactory,
|
||||
group: string,
|
||||
name: string,
|
||||
test: ConformanceTest,
|
||||
): TelemetryAdapterConformanceCase {
|
||||
return {
|
||||
group,
|
||||
name,
|
||||
async run() {
|
||||
await using fixture = await factory();
|
||||
await test(fixture);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findSpan(spans: readonly RecordedTelemetrySpan[], name: string): RecordedTelemetrySpan {
|
||||
const span = spans.find((candidate) => candidate.name === name);
|
||||
ok(span, `Expected recorded span ${name}`);
|
||||
return span;
|
||||
}
|
||||
|
||||
async function rejectsWithSameValue(operation: Promise<unknown>, expected: unknown): Promise<void> {
|
||||
try {
|
||||
await operation;
|
||||
fail("Expected operation to reject");
|
||||
} catch (error) {
|
||||
strictEqual(error, expected);
|
||||
}
|
||||
}
|
||||
|
||||
function unreadable<T extends object>(value: T): T {
|
||||
return new Proxy(value, {
|
||||
get: () => {
|
||||
throw new Error("read");
|
||||
},
|
||||
getOwnPropertyDescriptor: () => {
|
||||
throw new Error("inspect");
|
||||
},
|
||||
getPrototypeOf: () => {
|
||||
throw new Error("prototype");
|
||||
},
|
||||
ownKeys: () => {
|
||||
throw new Error("enumerate");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates runner-independent cases for the callback telemetry adapter contract. */
|
||||
export function createTelemetryAdapterConformance(
|
||||
factory: TelemetryAdapterFixtureFactory,
|
||||
): readonly TelemetryAdapterConformanceCase[] {
|
||||
return [
|
||||
createCase(
|
||||
factory,
|
||||
"callback lifecycle",
|
||||
"admits once synchronously and preserves the result",
|
||||
async (fixture) => {
|
||||
let admitted = false;
|
||||
let calls = 0;
|
||||
const expected = { value: 42 };
|
||||
const result = fixture.context.startSpan({ name: "success" }, () => {
|
||||
admitted = true;
|
||||
calls++;
|
||||
return expected;
|
||||
});
|
||||
|
||||
strictEqual(admitted, true);
|
||||
strictEqual(calls, 1);
|
||||
strictEqual(await result, expected);
|
||||
deepStrictEqual(findSpan(await fixture.getSpans(), "success").status, { status: "ok" });
|
||||
strictEqual(findSpan(await fixture.getSpans(), "success").settled, true);
|
||||
},
|
||||
),
|
||||
|
||||
createCase(
|
||||
factory,
|
||||
"callback lifecycle",
|
||||
"preserves synchronous and asynchronous rejection values",
|
||||
async (fixture) => {
|
||||
const syncError = new Error("sync");
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "sync-error" }, () => {
|
||||
throw syncError;
|
||||
}),
|
||||
syncError,
|
||||
);
|
||||
|
||||
const asyncError = { kind: "async" };
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "async-error" }, async () => {
|
||||
throw asyncError;
|
||||
}),
|
||||
asyncError,
|
||||
);
|
||||
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "undefined-error" }, () => Promise.reject(undefined)),
|
||||
undefined,
|
||||
);
|
||||
|
||||
const unreadableError = unreadable({ kind: "unreadable" });
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "unreadable-error" }, () => {
|
||||
throw unreadableError;
|
||||
}),
|
||||
unreadableError,
|
||||
);
|
||||
|
||||
const asyncUnreadableError = unreadable({ kind: "async-unreadable" });
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "async-unreadable-error" }, () =>
|
||||
Promise.reject(asyncUnreadableError),
|
||||
),
|
||||
asyncUnreadableError,
|
||||
);
|
||||
|
||||
const spans = await fixture.getSpans();
|
||||
for (const name of [
|
||||
"sync-error",
|
||||
"async-error",
|
||||
"undefined-error",
|
||||
"unreadable-error",
|
||||
"async-unreadable-error",
|
||||
]) {
|
||||
strictEqual(findSpan(spans, name).status.status, "error");
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
createCase(factory, "status", "uses last explicit status without automatic overwrite", async (fixture) => {
|
||||
await fixture.context.startSpan({ name: "last-status" }, (span) => {
|
||||
span.setStatus({ status: "error", error: { name: "Expected", message: "first" } });
|
||||
span.setStatus({ status: "ok" });
|
||||
});
|
||||
|
||||
const thrown = new Error("after explicit status");
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "explicit-before-throw" }, (span) => {
|
||||
span.setStatus({ status: "ok" });
|
||||
throw thrown;
|
||||
}),
|
||||
thrown,
|
||||
);
|
||||
|
||||
const rejected = new Error("after async explicit status");
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "explicit-before-rejection" }, (span) => {
|
||||
span.setStatus({ status: "error", error: { name: "Expected", message: "async failure" } });
|
||||
return Promise.reject(rejected);
|
||||
}),
|
||||
rejected,
|
||||
);
|
||||
|
||||
await fixture.context.startSpan({ name: "expected-failure" }, (span) => {
|
||||
span.setStatus({ status: "error", error: { name: "Expected", message: "returned failure" } });
|
||||
return { ok: false };
|
||||
});
|
||||
|
||||
const spans = await fixture.getSpans();
|
||||
deepStrictEqual(findSpan(spans, "last-status").status, { status: "ok" });
|
||||
deepStrictEqual(findSpan(spans, "explicit-before-throw").status, { status: "ok" });
|
||||
deepStrictEqual(findSpan(spans, "explicit-before-rejection").status, {
|
||||
status: "error",
|
||||
error: { name: "Expected", message: "async failure" },
|
||||
});
|
||||
deepStrictEqual(findSpan(spans, "expected-failure").status, {
|
||||
status: "error",
|
||||
error: { name: "Expected", message: "returned failure" },
|
||||
});
|
||||
}),
|
||||
|
||||
createCase(factory, "recording", "merges attributes and records ordered events", async (fixture) => {
|
||||
await fixture.context.startSpan(
|
||||
{
|
||||
name: "recording",
|
||||
attributes: { start: "value", overwrite: "start", ignored: undefined },
|
||||
},
|
||||
(span) => {
|
||||
span.setAttributes({ count: 1, overwrite: "middle" });
|
||||
span.setAttributes({ count: undefined, overwrite: "end" });
|
||||
span.addEvent("first", { index: 1, ignored: undefined });
|
||||
span.addEvent("second", { index: 2 });
|
||||
},
|
||||
);
|
||||
|
||||
const span = findSpan(await fixture.getSpans(), "recording");
|
||||
deepStrictEqual(span.attributes, { start: "value", overwrite: "end", count: 1 });
|
||||
deepStrictEqual(span.events, [
|
||||
{ name: "first", attributes: { index: 1 } },
|
||||
{ name: "second", attributes: { index: 2 } },
|
||||
]);
|
||||
}),
|
||||
|
||||
createCase(factory, "recording", "ignores failed attribute calls atomically", async (fixture) => {
|
||||
await fixture.context.startSpan({ name: "atomic-attributes", attributes: { retained: "value" } }, (span) => {
|
||||
const attributes: SpanAttributes = {
|
||||
partial: "must not survive",
|
||||
unreadable: unreadable(["value"]),
|
||||
};
|
||||
doesNotThrow(() => span.setAttributes(attributes));
|
||||
});
|
||||
|
||||
deepStrictEqual(findSpan(await fixture.getSpans(), "atomic-attributes").attributes, {
|
||||
retained: "value",
|
||||
});
|
||||
}),
|
||||
|
||||
createCase(factory, "recording", "makes calls after settlement inert", async (fixture) => {
|
||||
let settledSpan: TelemetrySpan | undefined;
|
||||
await fixture.context.startSpan({ name: "settled", attributes: { value: "initial" } }, (span) => {
|
||||
settledSpan = span;
|
||||
});
|
||||
const capturedSpan = settledSpan;
|
||||
if (!capturedSpan) throw new Error("Expected callback span");
|
||||
|
||||
doesNotThrow(() => capturedSpan.setAttributes({ value: "late" }));
|
||||
doesNotThrow(() => capturedSpan.addEvent("late", { value: true }));
|
||||
doesNotThrow(() => capturedSpan.setStatus({ status: "error" }));
|
||||
let childAdmitted = false;
|
||||
const childResult = capturedSpan.startSpan({ name: "late-child" }, () => {
|
||||
childAdmitted = true;
|
||||
return 7;
|
||||
});
|
||||
strictEqual(childAdmitted, true);
|
||||
strictEqual(await childResult, 7);
|
||||
|
||||
const spans = await fixture.getSpans();
|
||||
strictEqual(spans.length, 1);
|
||||
deepStrictEqual(spans[0]?.attributes, { value: "initial" });
|
||||
deepStrictEqual(spans[0]?.events, []);
|
||||
deepStrictEqual(spans[0]?.status, { status: "ok" });
|
||||
}),
|
||||
|
||||
createCase(factory, "parentage", "records nested and concurrent child relationships", async (fixture) => {
|
||||
let releaseFirst: (() => void) | undefined;
|
||||
const firstGate = new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
|
||||
await fixture.context.startSpan({ name: "parent" }, async (parent) => {
|
||||
const first = parent.startSpan({ name: "first-child" }, async () => {
|
||||
await firstGate;
|
||||
});
|
||||
const second = parent.startSpan({ name: "second-child" }, () => "done");
|
||||
strictEqual(await second, "done");
|
||||
releaseFirst?.();
|
||||
await first;
|
||||
});
|
||||
|
||||
const spans = await fixture.getSpans();
|
||||
const parent = findSpan(spans, "parent");
|
||||
const first = findSpan(spans, "first-child");
|
||||
const second = findSpan(spans, "second-child");
|
||||
strictEqual(parent.parentId, null);
|
||||
strictEqual(first.parentId, parent.id);
|
||||
strictEqual(second.parentId, parent.id);
|
||||
ok(second.endSequence !== undefined && first.endSequence !== undefined && parent.endSequence !== undefined);
|
||||
ok(second.endSequence < first.endSequence);
|
||||
ok(first.endSequence < parent.endSequence);
|
||||
}),
|
||||
|
||||
createCase(factory, "passivity", "suppresses unreadable telemetry payload failures", async (fixture) => {
|
||||
let calls = 0;
|
||||
const options = unreadable<SpanOptions>({ name: "unreadable-options", attributes: { secret: "value" } });
|
||||
const result = fixture.context.startSpan(options, () => {
|
||||
calls++;
|
||||
return 9;
|
||||
});
|
||||
|
||||
strictEqual(calls, 1);
|
||||
strictEqual(await result, 9);
|
||||
deepStrictEqual(await fixture.getSpans(), []);
|
||||
|
||||
await fixture.context.startSpan({ name: "unreadable-recording" }, (span) => {
|
||||
const attributes = unreadable<SpanAttributes>({ secret: "value" });
|
||||
const status = unreadable<SpanStatus>({ status: "ok" });
|
||||
doesNotThrow(() => span.setAttributes(attributes));
|
||||
doesNotThrow(() => span.addEvent("unreadable-event", attributes));
|
||||
doesNotThrow(() => span.setStatus(status));
|
||||
});
|
||||
|
||||
const recorded = await fixture.getSpans();
|
||||
strictEqual(recorded.length, 1);
|
||||
deepStrictEqual(recorded[0]?.attributes, {});
|
||||
deepStrictEqual(recorded[0]?.events, []);
|
||||
deepStrictEqual(recorded[0]?.status, { status: "ok" });
|
||||
}),
|
||||
|
||||
createCase(factory, "passivity", "ignores failed status calls atomically", async (fixture) => {
|
||||
const rejection = new Error("rejected after unreadable status");
|
||||
await rejectsWithSameValue(
|
||||
fixture.context.startSpan({ name: "unreadable-status" }, (span) => {
|
||||
const status = unreadable<SpanStatus>({ status: "ok" });
|
||||
doesNotThrow(() => span.setStatus(status));
|
||||
return Promise.reject(rejection);
|
||||
}),
|
||||
rejection,
|
||||
);
|
||||
|
||||
strictEqual(findSpan(await fixture.getSpans(), "unreadable-status").status.status, "error");
|
||||
}),
|
||||
];
|
||||
}
|
||||
6
third_party/pi/packages/telemetry/src/testing/index.ts
vendored
Normal file
6
third_party/pi/packages/telemetry/src/testing/index.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export { createTelemetryAdapterConformance } from "./conformance.ts";
|
||||
export type {
|
||||
TelemetryAdapterConformanceCase,
|
||||
TelemetryAdapterFixture,
|
||||
TelemetryAdapterFixtureFactory,
|
||||
} from "./types.ts";
|
||||
18
third_party/pi/packages/telemetry/src/testing/types.ts
vendored
Normal file
18
third_party/pi/packages/telemetry/src/testing/types.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { TelemetryContext } from "../index.ts";
|
||||
import type { RecordedTelemetrySpan } from "../memory.ts";
|
||||
|
||||
/** A fresh adapter instance and normalized snapshot reader owned by one conformance case. */
|
||||
export interface TelemetryAdapterFixture extends AsyncDisposable {
|
||||
readonly context: TelemetryContext;
|
||||
getSpans(): Promise<readonly RecordedTelemetrySpan[]>;
|
||||
}
|
||||
|
||||
/** Creates an isolated adapter fixture for one conformance case. */
|
||||
export type TelemetryAdapterFixtureFactory = () => Promise<TelemetryAdapterFixture>;
|
||||
|
||||
/** A runner-independent conformance case that can be registered with any test framework. */
|
||||
export interface TelemetryAdapterConformanceCase {
|
||||
readonly group: string;
|
||||
readonly name: string;
|
||||
run(): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user