Skip to content

Commit 2e2beca

Browse files
committed
feat: use logger in core
1 parent 679742f commit 2e2beca

11 files changed

Lines changed: 395 additions & 63 deletions

File tree

packages/core/src/bridge/handlers/command-handlers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NodeHeartbeatPayload, NodeRegisterPayload } from '@frp-bridge/types'
1+
import type { NodeHeartbeatPayload, NodeRegisterPayload, ProxyConfig } from '@frp-bridge/types'
22
import type { NodeManager } from '../../node'
33
import type { FrpProcessManager } from '../../process'
44
import type { RpcServer } from '../../rpc'
@@ -530,9 +530,9 @@ const validateNodeDelete: Validator<NodeDeletePayload> = (payload) => {
530530
*/
531531
async function tunnelAddLocal(payload: TunnelAddPayload, deps: CommandDependencies): Promise<CommandResult> {
532532
// Convert TunnelAddPayload to ProxyConfig format
533-
const proxyConfig = {
533+
const proxyConfig: ProxyConfig = {
534534
name: payload.name,
535-
type: payload.type,
535+
type: payload.type as ProxyConfig['type'],
536536
localIP: '127.0.0.1',
537537
localPort: payload.localPort,
538538
...(payload.remotePort && { remotePort: payload.remotePort }),

packages/core/src/bridge/initializer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { homedir } from 'node:os'
77
import process from 'node:process'
88
import { consola } from 'consola'
99
import { join } from 'pathe'
10+
import { setGlobalLoggerOptions } from '../logging'
1011
import { ClientNodeCollector, FileNodeStorage, NodeManager } from '../node'
1112
import { FrpProcessManager } from '../process'
1213
import { RpcClient, RpcServer } from '../rpc'
@@ -76,6 +77,13 @@ export class FrpBridgeInitializer {
7677
*/
7778
initialize(): InitializationResult {
7879
const { rootWorkDir, runtimeDir, processDir } = this.setupDirectories()
80+
81+
// Configure global logger options with workspace root
82+
setGlobalLoggerOptions({
83+
workspaceRoot: rootWorkDir,
84+
enableFile: true
85+
})
86+
7987
const loggers = this.createLoggers()
8088

8189
const process = this.createProcessManager(processDir, loggers.processLogger)
@@ -102,7 +110,7 @@ export class FrpBridgeInitializer {
102110
* Setup and create working directories
103111
*/
104112
private setupDirectories(): { rootWorkDir: string, runtimeDir: string, processDir: string } {
105-
const rootWorkDir = this.config.workDir ?? join(homedir(), '.frp-bridge')
113+
const rootWorkDir = this.config.workDir ?? join(homedir(), '.frp-web')
106114
const runtimeDir = this.config.runtime?.workDir ?? join(rootWorkDir, 'runtime')
107115
const processDir = this.config.process?.workDir ?? join(rootWorkDir, 'process')
108116

packages/core/src/logging/index.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* Logging utility for FRP Bridge Core
3+
* Supports both console output and file logging with daily rotation
4+
*/
5+
6+
import { appendFileSync, existsSync, mkdirSync } from 'node:fs'
7+
import { homedir } from 'node:os'
8+
import { join } from 'node:path'
9+
10+
export type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error'
11+
12+
export interface LogData {
13+
[key: string]: unknown
14+
}
15+
16+
export interface LoggerOptions {
17+
level?: LogLevel
18+
dir?: string // Log directory, default workspace/.frp-web/logs
19+
workspaceRoot?: string // Workspace root dir, default homedir()
20+
enableConsole?: boolean // Enable console output, default true
21+
enableFile?: boolean // Enable file output, default true
22+
}
23+
24+
export interface Logger {
25+
debug: (message: string, data?: LogData) => void
26+
info: (message: string, data?: LogData) => void
27+
success: (message: string, data?: LogData) => void
28+
warn: (message: string, data?: LogData) => void
29+
error: (message: string, error?: Error | LogData) => void
30+
setLevel: (level: LogLevel) => void
31+
}
32+
33+
const LEVEL_ORDER: Record<LogLevel, number> = {
34+
debug: 0,
35+
info: 1,
36+
success: 1,
37+
warn: 2,
38+
error: 3
39+
}
40+
41+
const COLORS = {
42+
reset: '\x1B[0m',
43+
dim: '\x1B[2m',
44+
debug: '\x1B[36m',
45+
info: '\x1B[34m',
46+
success: '\x1B[32m',
47+
warn: '\x1B[33m',
48+
error: '\x1B[31m'
49+
} as const
50+
51+
function formatTime(date: Date): string {
52+
const yyyy = date.getFullYear()
53+
const MM = String(date.getMonth() + 1).padStart(2, '0')
54+
const dd = String(date.getDate()).padStart(2, '0')
55+
const HH = String(date.getHours()).padStart(2, '0')
56+
const mm = String(date.getMinutes()).padStart(2, '0')
57+
const ss = String(date.getSeconds()).padStart(2, '0')
58+
return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`
59+
}
60+
61+
function formatDateForFile(date: Date): string {
62+
const yyyy = date.getFullYear()
63+
const MM = String(date.getMonth() + 1).padStart(2, '0')
64+
const dd = String(date.getDate()).padStart(2, '0')
65+
return `${yyyy}-${MM}-${dd}`
66+
}
67+
68+
function padLevel(level: string): string {
69+
return level.padEnd(7)
70+
}
71+
72+
/**
73+
* Get default workspace root directory
74+
* Uses user's home directory as the base
75+
*/
76+
export function getDefaultWorkspaceRoot(): string {
77+
return homedir()
78+
}
79+
80+
/**
81+
* Resolve log directory to absolute path
82+
* - If dir is absolute, use it as-is
83+
* - If dir is relative, join with workspaceRoot
84+
* - Default is workspaceRoot/.frp-web/logs
85+
*/
86+
export function resolveLogDir(dir: string, workspaceRoot: string): string {
87+
if (dir.startsWith('/') || /^[a-z]:/i.test(dir)) {
88+
// Absolute path
89+
return dir
90+
}
91+
// Relative path, join with workspace root
92+
return join(workspaceRoot, dir)
93+
}
94+
95+
/**
96+
* File writer with daily rotation support
97+
*/
98+
class LogFileWriter {
99+
private currentDate: string
100+
private logFilePath: string
101+
private logDir: string
102+
103+
constructor(logDir: string) {
104+
this.logDir = logDir
105+
this.currentDate = formatDateForFile(new Date())
106+
this.ensureLogDir()
107+
this.logFilePath = this.getLogFilePath()
108+
}
109+
110+
private ensureLogDir(): void {
111+
if (!existsSync(this.logDir)) {
112+
mkdirSync(this.logDir, { recursive: true })
113+
}
114+
}
115+
116+
private getLogFilePath(): string {
117+
return join(this.logDir, `frp-bridge-${this.currentDate}.log`)
118+
}
119+
120+
write(message: string): void {
121+
const today = formatDateForFile(new Date())
122+
123+
// Check if date has changed, rotate log file if needed
124+
if (today !== this.currentDate) {
125+
this.currentDate = today
126+
this.logFilePath = this.getLogFilePath()
127+
}
128+
129+
try {
130+
appendFileSync(this.logFilePath, `${message}\n`, 'utf-8')
131+
}
132+
catch {
133+
// Silently fail to avoid infinite loop of errors
134+
// Console output will still show the error
135+
}
136+
}
137+
}
138+
139+
/**
140+
* Global logger options
141+
*/
142+
interface GlobalLoggerOptions {
143+
workspaceRoot?: string
144+
logDir?: string
145+
enableConsole?: boolean
146+
enableFile?: boolean
147+
}
148+
149+
let globalOptions: GlobalLoggerOptions = {}
150+
151+
/**
152+
* Set global logging options that will be used for all new loggers
153+
*/
154+
export function setGlobalLoggerOptions(options: GlobalLoggerOptions): void {
155+
globalOptions = { ...globalOptions, ...options }
156+
}
157+
158+
/**
159+
* Get global logging options
160+
*/
161+
export function getGlobalLoggerOptions(): GlobalLoggerOptions {
162+
return { ...globalOptions }
163+
}
164+
165+
/**
166+
* Create a logger instance with optional file output
167+
*/
168+
export function createLogger(tag: string, optionsOrLevel?: LogLevel | LoggerOptions): Logger {
169+
// Handle legacy API: createLogger(tag, level)
170+
let options: LoggerOptions = {}
171+
if (typeof optionsOrLevel === 'string') {
172+
options = { level: optionsOrLevel }
173+
}
174+
else {
175+
options = optionsOrLevel ?? {}
176+
}
177+
178+
const {
179+
level = 'info',
180+
dir = 'logs',
181+
workspaceRoot = globalOptions.workspaceRoot ?? getDefaultWorkspaceRoot(),
182+
enableConsole = true,
183+
enableFile = true
184+
} = options
185+
186+
// Resolve log directory to absolute path
187+
const resolvedLogDir = resolveLogDir(dir, workspaceRoot)
188+
189+
const currentLevelRef = { value: level }
190+
const fileWriter = enableFile ? new LogFileWriter(resolvedLogDir) : null
191+
192+
function createLogFunction(
193+
logTag: string,
194+
logLevel: LogLevel
195+
): (message: string, data?: LogData | Error) => void {
196+
return (message: string, data?: LogData | Error) => {
197+
if (LEVEL_ORDER[logLevel] < LEVEL_ORDER[currentLevelRef.value]) {
198+
return
199+
}
200+
201+
const timestamp = formatTime(new Date())
202+
const tagStr = `[${logTag}]`
203+
const levelStr = padLevel(logLevel.toUpperCase())
204+
205+
// Format data string
206+
let dataStr = ''
207+
if (data) {
208+
if (data instanceof Error) {
209+
dataStr = ` ${data.message}${data.stack ? `\n${data.stack}` : ''}`
210+
}
211+
else {
212+
dataStr = ` ${JSON.stringify(data)}`
213+
}
214+
}
215+
216+
// Plain log line for file (no colors)
217+
const plainMsg = `${timestamp} ${levelStr} ${tagStr} ${message}${dataStr}`
218+
219+
// Console output (with colors)
220+
if (enableConsole) {
221+
const color = COLORS[logLevel]
222+
const consoleMsg = `${COLORS.dim}${timestamp}${COLORS.reset} ${color}${levelStr}${COLORS.reset} ${tagStr} ${message}${dataStr}`
223+
// eslint-disable-next-line no-console
224+
console.log(consoleMsg)
225+
}
226+
227+
// File output
228+
if (fileWriter) {
229+
fileWriter.write(plainMsg)
230+
}
231+
}
232+
}
233+
234+
return {
235+
debug: createLogFunction(tag, 'debug'),
236+
info: createLogFunction(tag, 'info'),
237+
success: createLogFunction(tag, 'success'),
238+
warn: createLogFunction(tag, 'warn'),
239+
error: createLogFunction(tag, 'error'),
240+
setLevel(newLevel: LogLevel) {
241+
currentLevelRef.value = newLevel
242+
}
243+
}
244+
}

0 commit comments

Comments
 (0)