Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 25 additions & 136 deletions packages/bot/src/utils/monitoring/SimplifiedTelemetry.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) => 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<string, string>) => void }
commandDuration: {
observe: (labels: Record<string, string>, value: number) => void
}
interactions: { inc: (labels: Record<string, string>) => void }
musicActions: { inc: (labels: Record<string, string>) => void }
errors: { inc: (labels: Record<string, string>) => void }
}

export interface HealthCheckClient {
isHealthy: () => boolean
}

// Simplified span implementation
class SimplifiedSpan implements TelemetrySpan {
private readonly name: string
private readonly startTime: number
private attributes: Record<string, string> = {}
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<string, string>): 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<string, string>) => {
infoLog({ message: 'Command execution', data: labels })
},
}

commandDuration = {
observe: (labels: Record<string, string>, value: number) => {
debugLog({
message: 'Command duration',
data: { ...labels, duration: value },
})
},
}

interactions = {
inc: (labels: Record<string, string>) => {
infoLog({ message: 'Interaction', data: labels })
},
}

musicActions = {
inc: (labels: Record<string, string>) => {
infoLog({ message: 'Music action', data: labels })
},
}

errors = {
inc: (labels: Record<string, string>) => {
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,
Expand Down Expand Up @@ -173,25 +64,23 @@ 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,
recordMusicMetric,
recordErrorMetric,
} from './telemetryMetrics'

// Health check functions exported from ./healthChecks.ts
export {
checkRedisHealth,
checkDatabaseHealth,
Expand Down
122 changes: 122 additions & 0 deletions packages/bot/src/utils/monitoring/clients.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}
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<string, string>): 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<string, string>) => {
infoLog({ message: 'Command execution', data: labels })
},
}

commandDuration = {
observe: (labels: Record<string, string>, value: number) => {
debugLog({
message: 'Command duration',
data: { ...labels, duration: value },
})
},
}

interactions = {
inc: (labels: Record<string, string>) => {
infoLog({ message: 'Interaction', data: labels })
},
}

musicActions = {
inc: (labels: Record<string, string>) => {
infoLog({ message: 'Music action', data: labels })
},
}

errors = {
inc: (labels: Record<string, string>) => {
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()
2 changes: 1 addition & 1 deletion packages/bot/src/utils/monitoring/healthChecks.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/bot/src/utils/monitoring/telemetryMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CustomClient } from '../../types'
import { simplifiedMetrics } from './SimplifiedTelemetry'
import { simplifiedMetrics } from './clients'

export function recordCommandMetric(
client: CustomClient,
Expand Down
34 changes: 34 additions & 0 deletions packages/bot/src/utils/monitoring/telemetryTypes.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) => 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<string, string>) => void }
commandDuration: {
observe: (labels: Record<string, string>, value: number) => void
}
interactions: { inc: (labels: Record<string, string>) => void }
musicActions: { inc: (labels: Record<string, string>) => void }
errors: { inc: (labels: Record<string, string>) => void }
}

export interface HealthCheckClient {
isHealthy: () => boolean
}
Loading