Skip to content

Commit

Permalink
[KODO-12017] 添加生产模式输出详细日志功能及配置 (#499)
Browse files Browse the repository at this point in the history
  • Loading branch information
yinxulai committed Apr 21, 2021
1 parent 4a1da8c commit 850f8bb
Show file tree
Hide file tree
Showing 16 changed files with 458 additions and 111 deletions.
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`,不输出任何日志,本功能仅仅用于本地调试,不建议在线上环境开启。

* **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)
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) {
if (this.disableReport) return
try { reportV3(this.token, data, retry) }
catch (error) { console.warn(error) }
}

/**
* @param {unknown[]} ...args
* @description 输出 info 级别的调试信息。
*/
info(...args: unknown[]) {
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++) {
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)
}

0 comments on commit 850f8bb

Please sign in to comment.