Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[KODO-12017] 添加生产模式输出详细日志功能及配置 #499

Merged
merged 11 commits into from Apr 21, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
@@ -1,4 +1,5 @@
.DS_Store
.vscode
node_modules
bower_components
demo/config.js
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -201,6 +201,7 @@ qiniu.compressImage(file, options).then(data => {
* config.checkByMD5: 是否开启 MD5 校验,为布尔值;在断点续传时,开启 MD5 校验会将已上传的分片与当前分片进行 MD5 值比对,若不一致,则重传该分片,避免使用错误的分片。读取分片内容并计算 MD5 需要花费一定的时间,因此会稍微增加断点续传时的耗时,默认为 false,不开启。
* config.forceDirect: 是否上传全部采用直传方式,为布尔值;为 `true` 时则上传方式全部为直传 form 方式,禁用断点续传,默认 `false`。
* config.chunkSize: `number`,分片上传时每片的大小,必须为正整数,单位为 `MB`,且最大不能超过 1024,默认值 4。因为 chunk 数最大 10000,所以如果文件以你所设的 `chunkSize` 进行分片并且 chunk 数超过 10000,我们会把你所设的 `chunkSize` 扩大二倍,如果仍不符合则继续扩大,直到符合条件。
* config.debugLogLevel: `INFO` | `WARN` | `ERROR` | `OFF`,允许程序在控制台输出日志,默认为 `OFF`,不输出任何日志,本功能仅仅用于本地调试,不建议在线上环境开启。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本功能仅仅用于本地调试,不建议在线上环境开启

NIP: 也许将来可以考虑结合 process env

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我也考虑到了,后面可能会这么做


* **putExtra**: `object`,其中的每一项都为可选

Expand Down
9 changes: 4 additions & 5 deletions src/index.ts
@@ -1,11 +1,9 @@
import StatisticsLogger from './statisticsLog'
import createUploadManager, { Extra, Config, UploadOptions, UploadProgress } from './upload'
import { Observable, IObserver } from './observable'
import { CustomError } from './utils'
import { UploadCompleteData } from './api'
import compressImage from './compress'

const statisticsLogger = new StatisticsLogger()
import Logger from './logger'

/**
* @param file 上传文件
Expand All @@ -22,7 +20,6 @@ function upload(
putExtra?: Partial<Extra>,
config?: Partial<Config>
): Observable<UploadProgress, CustomError, UploadCompleteData> {

const options: UploadOptions = {
file,
key,
Expand All @@ -31,12 +28,14 @@ function upload(
config
}

// 为每个任务创建单独的 Logger
const logger = new Logger(token, config?.disableStatisticsReport, config?.debugLogLevel)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger constructor 的第二个参数名是 isReport,这边传的是 disable statistics report,是不是反了?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

要不要为每个任务的日志带上唯一的标识(类似 tracing 系统的 req id),以便在同时存在多个上传任务时区分不同任务的输出

return new Observable((observer: IObserver<UploadProgress, CustomError, UploadCompleteData>) => {
const manager = createUploadManager(options, {
onData: (data: UploadProgress) => observer.next(data),
onError: (err: CustomError) => observer.error(err),
onComplete: (res: any) => observer.complete(res)
}, statisticsLogger)
}, logger)
manager.putFile()
return manager.stop.bind(manager)
})
Expand Down
100 changes: 100 additions & 0 deletions src/logger/index.test.ts
@@ -0,0 +1,100 @@
import Logger from './index'

let isCallReport = false

jest.mock('./report-v3', () => ({
reportV3: () => {
isCallReport = true
}
}))

const originalLog = console.log
const originalWarn = console.warn
const originalError = console.error

const logMessage: unknown[] = []
const warnMessage: unknown[] = []
const errorMessage: unknown[] = []

beforeAll(() => {
console.log = jest.fn((...args: unknown[]) => logMessage.push(...args))
console.warn = jest.fn((...args: unknown[]) => warnMessage.push(...args))
console.error = jest.fn((...args: unknown[]) => errorMessage.push(...args))
})

afterAll(() => {
console.log = originalLog
console.warn = originalWarn
console.error = originalError
})

describe('test logger', () => {
test('test level', () => {
const infoLogger = new Logger('', true, 'INFO')
infoLogger.info('test1')
expect(logMessage).toStrictEqual([`Qiniu-JS-SDK [INFO][1]: `, 'test1'])
infoLogger.warn('test2')
expect(warnMessage).toStrictEqual(['Qiniu-JS-SDK [WARN][1]: ', 'test2'])
infoLogger.error('test3')
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][1]: ', 'test3'])

// 清空消息
logMessage.splice(0, logMessage.length)
warnMessage.splice(0, warnMessage.length)
errorMessage.splice(0, errorMessage.length)

const warnLogger = new Logger('', true, 'WARN')
warnLogger.info('test1')
expect(logMessage).toStrictEqual([])
warnLogger.warn('test2')
expect(warnMessage).toStrictEqual(['Qiniu-JS-SDK [WARN][2]: ', 'test2'])
warnLogger.error('test3')
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][2]: ', 'test3'])

// 清空消息
logMessage.splice(0, logMessage.length)
warnMessage.splice(0, warnMessage.length)
errorMessage.splice(0, errorMessage.length)

const errorLogger = new Logger('', true, 'ERROR')
errorLogger.info('test1')
expect(logMessage).toStrictEqual([])
errorLogger.warn('test2')
expect(warnMessage).toStrictEqual([])
errorLogger.error('test3')
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][3]: ', 'test3'])

// 清空消息
logMessage.splice(0, logMessage.length)
warnMessage.splice(0, warnMessage.length)
errorMessage.splice(0, errorMessage.length)

const offLogger = new Logger('', true, 'OFF')
offLogger.info('test1')
expect(logMessage).toStrictEqual([])
offLogger.warn('test2')
expect(warnMessage).toStrictEqual([])
offLogger.error('test3')
expect(errorMessage).toStrictEqual([])
})

test('test unique id', () => {
// @ts-ignore
const startId = Logger.id
new Logger('', true, 'OFF')
new Logger('', true, 'OFF')
const last = new Logger('', true, 'OFF')
// @ts-ignore
expect(last.id).toStrictEqual(startId + 3)
})

test('test report', () => {
const logger1 = new Logger('', false, 'OFF')
logger1.report(null as any)
expect(isCallReport).toBeTruthy()
isCallReport = false
const logger2 = new Logger('', true, 'OFF')
logger2.report(null as any)
expect(isCallReport).toBeFalsy()
})
})
61 changes: 61 additions & 0 deletions src/logger/index.ts
@@ -0,0 +1,61 @@
import { reportV3, V3LogInfo } from './report-v3'

export type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'OFF'

export default class Logger {
private static id: number = 0

// 为每个类分配一个 id
// 用以区分不同的上传任务
private id = ++Logger.id

constructor(
private token: string,
private disableReport = true,
private level: LogLevel = 'OFF'
) { }

/**
* @param {V3LogInfo} data 上报的数据。
* @param {boolean} retry 重试次数,可选,默认为 3。
* @description 向服务端上报统计信息。
*/
report(data: V3LogInfo, retry?: number) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIP: 这么看的话把 report 逻辑封装到 Logger 里来意义不大了

if (this.disableReport) return
try { reportV3(this.token, data, retry) }
catch (error) { console.warn(error) }
}

/**
* @param {unknown[]} ...args
* @description 输出 info 级别的调试信息。
*/
info(...args: unknown[]) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

const allowLevel: LogLevel[] = ['INFO']
if (allowLevel.includes(this.level)) {
console.log(`Qiniu-JS-SDK [INFO][${this.id}]: `, ...args)
}
}

/**
* @param {unknown[]} ...args
* @description 输出 warn 级别的调试信息。
*/
warn(...args: unknown[]) {
const allowLevel: LogLevel[] = ['INFO', 'WARN']
if (allowLevel.includes(this.level)) {
console.warn(`Qiniu-JS-SDK [WARN][${this.id}]: `, ...args)
}
}

/**
* @param {unknown[]} ...args
* @description 输出 error 级别的调试信息。
*/
error(...args: unknown[]) {
const allowLevel: LogLevel[] = ['INFO', 'WARN', 'ERROR']
if (allowLevel.includes(this.level)) {
console.error(`Qiniu-JS-SDK [ERROR][${this.id}]: `, ...args)
}
}
}
103 changes: 103 additions & 0 deletions src/logger/report-v3.test.ts
@@ -0,0 +1,103 @@
import { reportV3, V3LogInfo } from './report-v3'

class MockXHR {
sendData: string
openData: string[]
openCount: number
headerData: string[]

status: number
readyState: number
onreadystatechange() { }

clear() {
this.sendData = ''
this.openData = []
this.headerData = []

this.status = 0
this.readyState = 0
}

open(...args: string[]) {
this.clear()
this.openCount += 1
this.openData = args
}

send(args: string) {
this.sendData = args
}

setRequestHeader(...args: string[]) {
this.headerData.push(...args)
}

changeStatusAndState(readyState: number, status: number) {
this.status = status
this.readyState = readyState
this.onreadystatechange()
}
}

const mockXHR = new MockXHR()

jest.mock('../utils', () => ({
createXHR: () => mockXHR,
getAuthHeaders: (t: string) => t
}))

describe('test report-v3', () => {
const testData: V3LogInfo = {
code: 200,
reqId: 'reqId',
host: 'host',
remoteIp: 'remoteIp',
port: 'port',
duration: 1,
time: 1,
bytesSent: 1,
upType: 'jssdk-h5',
size: 1
}

test('test stringify send Data', () => {
reportV3('token', testData, 3)
mockXHR.changeStatusAndState(0, 0)
expect(mockXHR.sendData).toBe([
testData.code || '',
testData.reqId || '',
testData.host || '',
testData.remoteIp || '',
testData.port || '',
testData.duration || '',
testData.time || '',
testData.bytesSent || '',
testData.upType || '',
testData.size || ''
].join(','))
})

test('test retry', () => {
mockXHR.openCount = 0
reportV3('token', testData)
for (let index = 1; index <= 10; index++) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 次是随便定义的,大于传的 retry 次数就行

mockXHR.changeStatusAndState(4, 0)
}
expect(mockXHR.openCount).toBe(4)

mockXHR.openCount = 0
reportV3('token', testData, 4)
for (let index = 1; index < 10; index++) {
mockXHR.changeStatusAndState(4, 0)
}
expect(mockXHR.openCount).toBe(5)

mockXHR.openCount = 0
reportV3('token', testData, 0)
for (let index = 1; index < 10; index++) {
mockXHR.changeStatusAndState(4, 0)
}
expect(mockXHR.openCount).toBe(1)
})
})
48 changes: 48 additions & 0 deletions src/logger/report-v3.ts
@@ -0,0 +1,48 @@
import { createXHR, getAuthHeaders } from '../utils'

export interface V3LogInfo {
code: number
reqId: string
host: string
remoteIp: string
port: string
duration: number
time: number
bytesSent: number
upType: 'jssdk-h5'
size: number
}

/**
* @param {string} token 上传使用的 token
* @param {V3LogInfo} data 上报的统计数据
* @param {number} retry 重试的次数,默认值 3
* @description v3 版本的日志上传接口,参考文档 https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3。
*/
export function reportV3(token: string, data: V3LogInfo, retry = 3) {
const xhr = createXHR()
xhr.open('POST', 'https://uplog.qbox.me/log/3')
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xhr.setRequestHeader('Authorization', getAuthHeaders(token).Authorization)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status !== 200 && retry > 0) {
reportV3(token, data, retry - 1)
}
}

// 顺序参考:https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3
const stringifyData = [
data.code || '',
data.reqId || '',
data.host || '',
data.remoteIp || '',
data.port || '',
data.duration || '',
data.time || '',
data.bytesSent || '',
data.upType || '',
data.size || ''
].join(',')

xhr.send(stringifyData)
}