From 7f4b6e1fa908de8f24583379d67a5e828ca64284 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sat, 16 May 2026 00:37:44 -0300 Subject: [PATCH] refactor(bot): break monitoring telemetry cycles (#871 PR 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts singletons + classes from `SimplifiedTelemetry` into `clients.ts` and shared interfaces into `telemetryTypes.ts`, so `healthChecks.ts` and `telemetryMetrics.ts` no longer import back through `SimplifiedTelemetry` to grab their dependencies. Madge cycle count: 12 → 10 (Cluster B done; remaining are Cluster C autoplay/queue + the type-only `types/CustomClient` cycle deferred to follow-up PRs). Public re-exports preserved on `SimplifiedTelemetry` for backwards compatibility with `SimplifiedTelemetryWrapper`, `telemetry`, `health`, `metrics`. No behavior changes. Plan: .claude/plans/2026-05-16-bot-circular-deps.md (Phase 2) ADR: docs/decisions/2026-05-16-next-refactor-target-bot-circular-deps.md --- .../utils/monitoring/SimplifiedTelemetry.ts | 161 +++--------------- packages/bot/src/utils/monitoring/clients.ts | 122 +++++++++++++ .../bot/src/utils/monitoring/healthChecks.ts | 2 +- .../src/utils/monitoring/telemetryMetrics.ts | 2 +- .../src/utils/monitoring/telemetryTypes.ts | 34 ++++ 5 files changed, 183 insertions(+), 138 deletions(-) create mode 100644 packages/bot/src/utils/monitoring/clients.ts create mode 100644 packages/bot/src/utils/monitoring/telemetryTypes.ts diff --git a/packages/bot/src/utils/monitoring/SimplifiedTelemetry.ts b/packages/bot/src/utils/monitoring/SimplifiedTelemetry.ts index d730b0740..769167ca5 100644 --- a/packages/bot/src/utils/monitoring/SimplifiedTelemetry.ts +++ b/packages/bot/src/utils/monitoring/SimplifiedTelemetry.ts @@ -1,145 +1,36 @@ /** - * Simplified telemetry system without OpenTelemetry dependency + * Simplified telemetry system without OpenTelemetry dependency. + * + * Class implementations + singletons live in `./clients`; interfaces + * live in `./telemetryTypes`. This file is now a thin facade for span + * creation + the public re-export surface kept for backwards + * compatibility with consumers that imported everything from here + * (`SimplifiedTelemetryWrapper`, `telemetry`, `health`, `metrics`). + * + * See docs/decisions/2026-05-16-next-refactor-target-bot-circular-deps.md + * for the cycle-break rationale. */ import type { ChatInputCommandInteraction, Interaction } from 'discord.js' import type { CustomClient } from '../../types' -import { infoLog, errorLog, debugLog } from '@lucky/shared/utils' -export interface TelemetrySpan { - setAttributes: (attrs: Record) => void - setAttribute: (key: string, value: string) => void - setStatus: (status: { code: number; message?: string }) => void - end: () => void - recordException: (error: Error) => void -} - -export interface TelemetryTracer { - startSpan: (name: string) => TelemetrySpan -} - -export interface MetricsClient { - commandExecutions: { inc: (labels: Record) => void } - commandDuration: { - observe: (labels: Record, value: number) => void - } - interactions: { inc: (labels: Record) => void } - musicActions: { inc: (labels: Record) => void } - errors: { inc: (labels: Record) => void } -} - -export interface HealthCheckClient { - isHealthy: () => boolean -} - -// Simplified span implementation -class SimplifiedSpan implements TelemetrySpan { - private readonly name: string - private readonly startTime: number - private attributes: Record = {} - private status: { code: number; message?: string } = { code: 1 } - private ended = false - - constructor(name: string) { - this.name = name - this.startTime = Date.now() - debugLog({ message: `Started span: ${name}` }) - } - - setAttributes(attrs: Record): void { - this.attributes = { ...this.attributes, ...attrs } - } - - setAttribute(key: string, value: string): void { - this.attributes[key] = value - } - - setStatus(status: { code: number; message?: string }): void { - this.status = status - } - - end(): void { - if (this.ended) return +export type { + TelemetrySpan, + TelemetryTracer, + MetricsClient, + HealthCheckClient, +} from './telemetryTypes' - const duration = Date.now() - this.startTime - this.ended = true +import type { TelemetrySpan } from './telemetryTypes' - debugLog({ - message: `Ended span: ${this.name}`, - data: { - duration, - status: this.status.code, - attributes: this.attributes, - }, - }) - } - - recordException(error: Error): void { - errorLog({ - message: `Exception in span: ${this.name}`, - error, - data: this.attributes, - }) - this.setStatus({ code: 2, message: error.message }) - } -} - -// Simplified tracer implementation -class SimplifiedTracer implements TelemetryTracer { - startSpan(name: string): TelemetrySpan { - return new SimplifiedSpan(name) - } -} - -// Simplified metrics client -class SimplifiedMetricsClient implements MetricsClient { - commandExecutions = { - inc: (labels: Record) => { - infoLog({ message: 'Command execution', data: labels }) - }, - } - - commandDuration = { - observe: (labels: Record, value: number) => { - debugLog({ - message: 'Command duration', - data: { ...labels, duration: value }, - }) - }, - } - - interactions = { - inc: (labels: Record) => { - infoLog({ message: 'Interaction', data: labels }) - }, - } - - musicActions = { - inc: (labels: Record) => { - infoLog({ message: 'Music action', data: labels }) - }, - } - - errors = { - inc: (labels: Record) => { - errorLog({ message: 'Error metric', data: labels }) - }, - } -} - -// Simplified health check client -class SimplifiedHealthCheckClient implements HealthCheckClient { - isHealthy(): boolean { - return true // Simplified - always healthy - } -} +export { + simplifiedTracer, + simplifiedMetrics, + simplifiedHealthCheck, +} from './clients' -// Export singleton instances -export const simplifiedTracer = new SimplifiedTracer() -export const simplifiedMetrics = new SimplifiedMetricsClient() -export const simplifiedHealthCheck = new SimplifiedHealthCheckClient() +import { simplifiedTracer } from './clients' -// Telemetry functions export function createCommandSpan( interaction: ChatInputCommandInteraction, _client: CustomClient, @@ -173,17 +64,16 @@ export function createInteractionSpan( } export function markSpanSuccess(span: TelemetrySpan): void { - span.setStatus({ code: 1 }) // OK + span.setStatus({ code: 1 }) span.end() } export function markSpanError(span: TelemetrySpan, error: Error): void { - span.setStatus({ code: 2, message: error.message }) // ERROR + span.setStatus({ code: 2, message: error.message }) span.recordException(error) span.end() } -// Metrics recording functions exported from ./telemetryMetrics.ts export { recordCommandMetric, recordInteractionMetric, @@ -191,7 +81,6 @@ export { recordErrorMetric, } from './telemetryMetrics' -// Health check functions exported from ./healthChecks.ts export { checkRedisHealth, checkDatabaseHealth, diff --git a/packages/bot/src/utils/monitoring/clients.ts b/packages/bot/src/utils/monitoring/clients.ts new file mode 100644 index 000000000..af3c906b9 --- /dev/null +++ b/packages/bot/src/utils/monitoring/clients.ts @@ -0,0 +1,122 @@ +/** + * Singleton client instances for the simplified telemetry system. + * + * Extracted from `./SimplifiedTelemetry` to break the runtime cycles + * `SimplifiedTelemetry ↔ healthChecks` and `SimplifiedTelemetry ↔ + * telemetryMetrics` (madge cycles 3, 4 of the original list). See + * docs/decisions/2026-05-16-next-refactor-target-bot-circular-deps.md. + * + * Interfaces live in `./telemetryTypes` so this module has no + * dependency on `./SimplifiedTelemetry`. + */ + +import { infoLog, errorLog, debugLog } from '@lucky/shared/utils' +import type { + TelemetrySpan, + TelemetryTracer, + MetricsClient, + HealthCheckClient, +} from './telemetryTypes' + +class SimplifiedSpan implements TelemetrySpan { + private readonly name: string + private readonly startTime: number + private attributes: Record = {} + private status: { code: number; message?: string } = { code: 1 } + private ended = false + + constructor(name: string) { + this.name = name + this.startTime = Date.now() + debugLog({ message: `Started span: ${name}` }) + } + + setAttributes(attrs: Record): void { + this.attributes = { ...this.attributes, ...attrs } + } + + setAttribute(key: string, value: string): void { + this.attributes[key] = value + } + + setStatus(status: { code: number; message?: string }): void { + this.status = status + } + + end(): void { + if (this.ended) return + + const duration = Date.now() - this.startTime + this.ended = true + + debugLog({ + message: `Ended span: ${this.name}`, + data: { + duration, + status: this.status.code, + attributes: this.attributes, + }, + }) + } + + recordException(error: Error): void { + errorLog({ + message: `Exception in span: ${this.name}`, + error, + data: this.attributes, + }) + this.setStatus({ code: 2, message: error.message }) + } +} + +class SimplifiedTracer implements TelemetryTracer { + startSpan(name: string): TelemetrySpan { + return new SimplifiedSpan(name) + } +} + +class SimplifiedMetricsClient implements MetricsClient { + commandExecutions = { + inc: (labels: Record) => { + infoLog({ message: 'Command execution', data: labels }) + }, + } + + commandDuration = { + observe: (labels: Record, value: number) => { + debugLog({ + message: 'Command duration', + data: { ...labels, duration: value }, + }) + }, + } + + interactions = { + inc: (labels: Record) => { + infoLog({ message: 'Interaction', data: labels }) + }, + } + + musicActions = { + inc: (labels: Record) => { + infoLog({ message: 'Music action', data: labels }) + }, + } + + errors = { + inc: (labels: Record) => { + errorLog({ message: 'Error metric', data: labels }) + }, + } +} + +class SimplifiedHealthCheckClient implements HealthCheckClient { + isHealthy(): boolean { + return true + } +} + +export const simplifiedTracer: TelemetryTracer = new SimplifiedTracer() +export const simplifiedMetrics: MetricsClient = new SimplifiedMetricsClient() +export const simplifiedHealthCheck: HealthCheckClient = + new SimplifiedHealthCheckClient() diff --git a/packages/bot/src/utils/monitoring/healthChecks.ts b/packages/bot/src/utils/monitoring/healthChecks.ts index e8b4acbf9..a2ffe8d76 100644 --- a/packages/bot/src/utils/monitoring/healthChecks.ts +++ b/packages/bot/src/utils/monitoring/healthChecks.ts @@ -1,5 +1,5 @@ import type { CustomClient } from '../../types' -import { simplifiedHealthCheck } from './SimplifiedTelemetry' +import { simplifiedHealthCheck } from './clients' export function checkRedisHealth(_client: CustomClient): boolean { if (!_client.redis) return false diff --git a/packages/bot/src/utils/monitoring/telemetryMetrics.ts b/packages/bot/src/utils/monitoring/telemetryMetrics.ts index e053b1110..f626b8dac 100644 --- a/packages/bot/src/utils/monitoring/telemetryMetrics.ts +++ b/packages/bot/src/utils/monitoring/telemetryMetrics.ts @@ -1,5 +1,5 @@ import type { CustomClient } from '../../types' -import { simplifiedMetrics } from './SimplifiedTelemetry' +import { simplifiedMetrics } from './clients' export function recordCommandMetric( client: CustomClient, diff --git a/packages/bot/src/utils/monitoring/telemetryTypes.ts b/packages/bot/src/utils/monitoring/telemetryTypes.ts new file mode 100644 index 000000000..6766c4e1d --- /dev/null +++ b/packages/bot/src/utils/monitoring/telemetryTypes.ts @@ -0,0 +1,34 @@ +/** + * Shared types for the simplified telemetry system. + * + * Extracted so `./clients` can implement these interfaces without + * depending on `./SimplifiedTelemetry`, which would re-introduce the + * runtime cycle this refactor removes. See + * docs/decisions/2026-05-16-next-refactor-target-bot-circular-deps.md. + */ + +export interface TelemetrySpan { + setAttributes: (attrs: Record) => void + setAttribute: (key: string, value: string) => void + setStatus: (status: { code: number; message?: string }) => void + end: () => void + recordException: (error: Error) => void +} + +export interface TelemetryTracer { + startSpan: (name: string) => TelemetrySpan +} + +export interface MetricsClient { + commandExecutions: { inc: (labels: Record) => void } + commandDuration: { + observe: (labels: Record, value: number) => void + } + interactions: { inc: (labels: Record) => void } + musicActions: { inc: (labels: Record) => void } + errors: { inc: (labels: Record) => void } +} + +export interface HealthCheckClient { + isHealthy: () => boolean +}