153 lines
3.7 KiB
TypeScript
153 lines
3.7 KiB
TypeScript
import type { LogLine } from '@types'
|
|
import { get, handleError, makeUrl } from '../http'
|
|
import { resolveAppName } from '../name'
|
|
|
|
interface LogOptions {
|
|
date?: string
|
|
follow?: boolean
|
|
grep?: string
|
|
since?: string
|
|
}
|
|
|
|
const formatDate = (date: Date) =>
|
|
date.toISOString().slice(0, 10)
|
|
|
|
const matchesGrep = (text: string, pattern: string) =>
|
|
text.toLowerCase().includes(pattern.toLowerCase())
|
|
|
|
const parseDuration = (duration: string): number | null => {
|
|
const match = duration.match(/^(\d+)([hdwm])$/)
|
|
if (!match) return null
|
|
|
|
const value = parseInt(match[1]!, 10)
|
|
const unit = match[2]!
|
|
|
|
const ms = {
|
|
h: 60 * 60 * 1000,
|
|
d: 24 * 60 * 60 * 1000,
|
|
w: 7 * 24 * 60 * 60 * 1000,
|
|
m: 30 * 24 * 60 * 60 * 1000,
|
|
}
|
|
|
|
return value * ms[unit as keyof typeof ms]
|
|
}
|
|
|
|
const printDiskLog = (line: string, grep?: string) => {
|
|
if (grep && !matchesGrep(line, grep)) return
|
|
console.log(line)
|
|
}
|
|
|
|
export const printLog = (line: LogLine, grep?: string) => {
|
|
if (grep && !matchesGrep(line.text, grep)) return
|
|
console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`)
|
|
}
|
|
|
|
export async function logApp(arg: string | undefined, options: LogOptions) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
|
|
if (options.follow) {
|
|
await tailLogs(name, options.grep)
|
|
return
|
|
}
|
|
|
|
// Handle --since option
|
|
if (options.since) {
|
|
const ms = parseDuration(options.since)
|
|
if (!ms) {
|
|
console.error('Invalid duration. Use format like: 1h, 2d, 1w, 1m')
|
|
return
|
|
}
|
|
|
|
const dates = await get<string[]>(`/api/apps/${name}/logs/dates`)
|
|
if (!dates) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
|
|
const cutoff = new Date(Date.now() - ms)
|
|
const cutoffDate = formatDate(cutoff)
|
|
|
|
// Filter dates that are >= cutoff
|
|
const relevantDates = dates.filter(d => d >= cutoffDate).reverse()
|
|
if (relevantDates.length === 0) {
|
|
console.log('No logs in the specified time range')
|
|
return
|
|
}
|
|
|
|
for (const date of relevantDates) {
|
|
const lines = await get<string[]>(`/api/apps/${name}/logs?date=${date}`)
|
|
if (!lines) continue
|
|
for (const line of lines) {
|
|
printDiskLog(line, options.grep)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle --date option
|
|
if (options.date) {
|
|
const lines = await get<string[]>(`/api/apps/${name}/logs?date=${options.date}`)
|
|
if (!lines) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
if (lines.length === 0) {
|
|
console.log('No logs for this date')
|
|
return
|
|
}
|
|
for (const line of lines) {
|
|
printDiskLog(line, options.grep)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Default: show today's in-memory logs
|
|
const logs: LogLine[] | undefined = await get(`/api/apps/${name}/logs`)
|
|
if (!logs) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
if (logs.length === 0) {
|
|
console.log('No logs yet')
|
|
return
|
|
}
|
|
for (const line of logs) {
|
|
printLog(line, options.grep)
|
|
}
|
|
}
|
|
|
|
export async function tailLogs(name: string, grep?: string) {
|
|
try {
|
|
const url = makeUrl(`/api/apps/${name}/logs/stream`)
|
|
const res = await fetch(url)
|
|
if (!res.ok) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
if (!res.body) return
|
|
|
|
const reader = res.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buffer = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split('\n\n')
|
|
buffer = lines.pop() ?? ''
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
const data = JSON.parse(line.slice(6)) as LogLine
|
|
printLog(data, grep)
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|