Skip to content

Commit af1c081

Browse files
committedJun 13, 2023
feat(logger): update dump strategy, fix koishijs#50
1 parent 9453d1f commit af1c081

File tree

3 files changed

+144
-72
lines changed

3 files changed

+144
-72
lines changed
 

‎plugins/logger/client/index.vue

+42-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
<k-layout>
33
<el-scrollbar class="container">
44
<div ref="root" class="logs">
5-
<div class="line" :class="{ start: line.includes(hint) }" v-for="line in store.logs">
6-
<code v-html="renderLine(line)"></code>
5+
<div
6+
v-for="(record, index) in store.logs"
7+
:key="record.id"
8+
class="line" :class="{ start: index && store.logs[index - 1].id > record.id && record.name === 'app' }">
9+
<code v-html="renderLine(record)"></code>
710
</div>
811
</div>
912
</el-scrollbar>
@@ -13,18 +16,50 @@
1316
<script lang="ts" setup>
1417
1518
import { watch, ref, nextTick, onActivated } from 'vue'
16-
import { store } from '@koishijs/client'
19+
import type { Logger } from 'koishi'
20+
import { store, Time } from '@koishijs/client'
1721
import ansi from 'ansi_up'
1822
1923
const root = ref<HTMLElement>()
2024
21-
const hint = `app\u001b[0m \u001b[38;5;15;1mKoishi/`
22-
2325
// this package does not have consistent exports in different environments
2426
const converter = new (ansi['default'] || ansi)()
2527
26-
function renderLine(line: string) {
27-
return converter.ansi_to_html(line)
28+
function renderColor(code: number, value: any, decoration = '') {
29+
return `\u001b[3${code < 8 ? code : '8;5;' + code}${decoration}m${value}\u001b[0m`
30+
}
31+
32+
function colorCode(name: string) {
33+
let hash = 0
34+
for (let i = 0; i < name.length; i++) {
35+
hash = ((hash << 3) - hash) + name.charCodeAt(i)
36+
hash |= 0
37+
}
38+
return c256[Math.abs(hash) % c256.length]
39+
}
40+
41+
const showTime = 'yyyy-MM-dd hh:mm:ss'
42+
43+
const c256 = [
44+
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
45+
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
46+
129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
47+
169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
48+
201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
49+
]
50+
51+
function renderLine(record: Logger.Record) {
52+
const prefix = `[${record.type[0].toUpperCase()}]`
53+
const space = ' '
54+
let indent = 3 + space.length, output = ''
55+
indent += showTime.length + space.length
56+
output += renderColor(8, Time.template(showTime)) + space
57+
const code = colorCode(record.name)
58+
const label = renderColor(code, record.name, ';1')
59+
const padLength = label.length - record.name.length
60+
output += prefix + space + label.padEnd(padLength) + space
61+
output += record.content.replace(/\n/g, '\n' + ' '.repeat(indent))
62+
return converter.ansi_to_html(output)
2863
}
2964
3065
onActivated(() => {

‎plugins/logger/src/file.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FileHandle, open } from 'fs/promises'
2+
import { Logger } from 'koishi'
3+
4+
export class FileWriter {
5+
public data: Logger.Record[]
6+
public task: Promise<FileHandle>
7+
public size: number
8+
9+
private temp: Logger.Record[] = []
10+
11+
constructor(public date: string, public path: string) {
12+
this.task = open(path, 'a+').then(async (handle) => {
13+
const buffer = await handle.readFile()
14+
this.data = this.parse(new TextDecoder().decode(buffer))
15+
this.size = buffer.byteLength
16+
return handle
17+
})
18+
this.task.then(() => this.flush())
19+
}
20+
21+
flush() {
22+
if (!this.temp.length) return
23+
this.task = this.task.then(async (handle) => {
24+
const content = Buffer.from(this.temp.map((record) => JSON.stringify(record) + '\n').join(''))
25+
await handle.write(content)
26+
this.data.push(...this.temp)
27+
this.size += content.byteLength
28+
this.temp = []
29+
return handle
30+
})
31+
}
32+
33+
parse(text: string) {
34+
return text.split('\n').map((line) => {
35+
try {
36+
return JSON.parse(line)
37+
} catch {}
38+
}).filter(Boolean)
39+
}
40+
41+
async read() {
42+
await this.task
43+
return this.data
44+
}
45+
46+
write(record: Logger.Record) {
47+
this.temp.push(record)
48+
this.flush()
49+
}
50+
51+
async close() {
52+
const handle = await this.task
53+
await handle.close()
54+
}
55+
}

‎plugins/logger/src/index.ts

+47-65
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { Context, Logger, remove, Schema, Time } from 'koishi'
1+
import { Context, Dict, Logger, remove, Schema, Time } from 'koishi'
22
import { DataService } from '@koishijs/plugin-console'
33
import { resolve } from 'path'
4-
import { promises as fsp, mkdirSync, readdirSync } from 'fs'
5-
import { FileHandle } from 'fs/promises'
6-
7-
const { open, rm } = fsp
4+
import { mkdirSync, readdirSync } from 'fs'
5+
import { rm } from 'fs/promises'
6+
import { FileWriter } from './file'
87

98
declare module '@koishijs/plugin-console' {
109
namespace Console {
@@ -14,10 +13,10 @@ declare module '@koishijs/plugin-console' {
1413
}
1514
}
1615

17-
class LogProvider extends DataService<string[]> {
16+
class LogProvider extends DataService<Logger.Record[]> {
1817
root: string
1918
date: string
20-
files: number[] = []
19+
files: Dict<number> = {}
2120
writer: FileWriter
2221

2322
constructor(ctx: Context, private config: LogProvider.Config = {}) {
@@ -28,55 +27,57 @@ class LogProvider extends DataService<string[]> {
2827
prod: resolve(__dirname, '../dist'),
2928
})
3029

31-
this.ctx.on('ready', () => {
30+
ctx.on('ready', () => {
3231
this.prepareWriter()
3332
this.prepareLogger()
3433
}, true)
34+
35+
ctx.on('dispose', () => {
36+
this.writer?.close()
37+
this.writer = null
38+
})
3539
}
3640

3741
prepareWriter() {
3842
this.root = resolve(this.ctx.baseDir, this.config.root)
3943
mkdirSync(this.root, { recursive: true })
4044

4145
for (const filename of readdirSync(this.root)) {
42-
if (!filename.endsWith('.log')) continue
43-
this.files.push(Time.getDateNumber(new Date(filename.slice(0, -4)), 0))
46+
const capture = /^(\d{4}-\d{2}-\d{2})-(\d+)\.log$/.exec(filename)
47+
if (!capture) continue
48+
this.files[capture[1]] = Math.max(this.files[capture[1]] ?? 0, +capture[2])
4449
}
4550

46-
this.createFile()
47-
48-
this.ctx.on('dispose', () => {
49-
this.writer?.close()
50-
this.writer = null
51-
})
51+
const date = new Date().toISOString().slice(0, 10)
52+
this.createFile(date, this.files[date] ??= 1)
5253
}
5354

54-
createFile() {
55-
this.date = Time.template('yyyy-MM-dd')
56-
this.writer = new FileWriter(`${this.root}/${this.date}.log`)
55+
async createFile(date: string, index: number) {
56+
this.writer = new FileWriter(date, `${this.root}/${date}-${index}.log`)
5757

5858
const { maxAge } = this.config
5959
if (!maxAge) return
6060

61-
const current = Time.getDateNumber(new Date(), 0)
62-
this.files = this.files.filter((date) => {
63-
if (date >= current - maxAge) return true
64-
rm(`${this.root}/${Time.template('yyyy-MM-dd', Time.fromDateNumber(date, 0))}.log`)
65-
})
61+
const now = Date.now()
62+
for (const date in this.files) {
63+
if (now - +new Date(date) < maxAge * Time.day) continue
64+
for (let index = 1; index <= this.files[date]; ++index) {
65+
await rm(`${this.root}/${date}-${index}.log`)
66+
}
67+
}
6668
}
6769

6870
prepareLogger() {
69-
if (this.ctx.prologue) {
70-
for (const line of this.ctx.prologue) {
71-
this.printText(line)
71+
if (this.ctx.loader.prolog) {
72+
for (const record of this.ctx.loader.prolog) {
73+
this.record(record)
7274
}
73-
this.ctx.root.prologue = null
75+
this.ctx.root.loader.prolog = null
7476
}
7577

7678
const target: Logger.Target = {
7779
colors: 3,
78-
showTime: 'yyyy-MM-dd hh:mm:ss',
79-
print: this.printText.bind(this),
80+
record: this.record.bind(this),
8081
}
8182

8283
Logger.targets.push(target)
@@ -86,13 +87,18 @@ class LogProvider extends DataService<string[]> {
8687
})
8788
}
8889

89-
printText(text: string) {
90-
if (!text.startsWith(this.date)) {
90+
record(record: Logger.Record) {
91+
const date = new Date(record.timestamp).toISOString().slice(0, 10)
92+
if (this.writer.date !== date) {
9193
this.writer.close()
92-
this.createFile()
94+
this.createFile(date, this.files[date] = 1)
95+
}
96+
this.writer.write(record)
97+
this.patch([record])
98+
if (this.writer.size >= this.config.maxSize) {
99+
this.writer.close()
100+
this.createFile(date, ++this.files[date])
93101
}
94-
this.writer.write(text)
95-
this.patch([text])
96102
}
97103

98104
async get() {
@@ -104,41 +110,17 @@ namespace LogProvider {
104110
export interface Config {
105111
root?: string
106112
maxAge?: number
113+
maxSize?: number
107114
}
108115

109116
export const Config: Schema<Config> = Schema.object({
110-
root: Schema.string().default('logs').description('存放输出日志的本地目录。'),
117+
root: Schema.path({
118+
filters: ['directory'],
119+
allowCreate: true,
120+
}).default('data/logs').description('存放输出日志的本地目录。'),
111121
maxAge: Schema.natural().default(30).description('日志文件保存的最大天数。'),
122+
maxSize: Schema.natural().default(1024 * 100).description('单个日志文件的最大大小。'),
112123
})
113124
}
114125

115126
export default LogProvider
116-
117-
class FileWriter {
118-
private task: Promise<FileHandle>
119-
private content: string[] = []
120-
121-
constructor(path: string) {
122-
this.task = open(path, 'a+').then(async (handle) => {
123-
const text = await handle.readFile('utf-8')
124-
if (text) this.content = text.split(/\n(?=\S)/g)
125-
return handle
126-
})
127-
}
128-
129-
async read() {
130-
await this.task
131-
return this.content
132-
}
133-
134-
async write(text: string) {
135-
const handle = await this.task
136-
await handle.write(text + '\n')
137-
this.content.push(text)
138-
}
139-
140-
async close() {
141-
const handle = await this.task
142-
await handle.close()
143-
}
144-
}

0 commit comments

Comments
 (0)
Please sign in to comment.