数数:日志模式用pinus-logger替代
This commit is contained in:
@@ -27,7 +27,7 @@ import * as redLockService from './app/services/redLockService';
|
|||||||
import _pinus = require('pinus');
|
import _pinus = require('pinus');
|
||||||
import { updateTeamStatus } from './app/services/comBattleService';
|
import { updateTeamStatus } from './app/services/comBattleService';
|
||||||
import { resResult, genCode } from './app/pubUtils/util';
|
import { resResult, genCode } from './app/pubUtils/util';
|
||||||
import { errlogger, infologger } from './app/util/logger';
|
import { errlogger, infologger, loadLogger } from './app/util/logger';
|
||||||
import { connectThinkingData, getTire } from './app/services/sdkService';
|
import { connectThinkingData, getTire } from './app/services/sdkService';
|
||||||
import { loadGmDb } from './app/db';
|
import { loadGmDb } from './app/db';
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -73,6 +73,7 @@ app.configure(function () {
|
|||||||
setupRoutes(app);
|
setupRoutes(app);
|
||||||
setupFilters(app);
|
setupFilters(app);
|
||||||
connectThinkingData(app);
|
connectThinkingData(app);
|
||||||
|
loadLogger(app.getServerId())
|
||||||
|
|
||||||
app.set(RESERVED.ERROR_HANDLER, errorHandler);
|
app.set(RESERVED.ERROR_HANDLER, errorHandler);
|
||||||
app.set(RESERVED.GLOBAL_ERROR_HANDLER, globalErrorHandler);
|
app.set(RESERVED.GLOBAL_ERROR_HANDLER, globalErrorHandler);
|
||||||
|
|||||||
393
game-server/app/services/sdk/ta.ts
Normal file
393
game-server/app/services/sdk/ta.ts
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
/**
|
||||||
|
* 模仿thinkingdata-node直接写一个log的写入,而日志直接用pinus-logger实现
|
||||||
|
*/
|
||||||
|
import { taLogger } from "../../util/logger";
|
||||||
|
|
||||||
|
const KEY_NAME_MATCH_REGEX = /^[a-zA-Z#][a-zA-Z0-9_]+$/;
|
||||||
|
|
||||||
|
let util: any = {};
|
||||||
|
util.version = '1.2.2';
|
||||||
|
util.each = function (obj, iterator, context) {
|
||||||
|
if (obj === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
|
||||||
|
obj.forEach(iterator, context);
|
||||||
|
} else if (obj.length === +obj.length) {
|
||||||
|
for (let i = 0, l = obj.length; i < l; i++) {
|
||||||
|
if (i in obj && iterator.call(context, obj[i], i, obj) === {}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
if (iterator.call(context, obj[key], key, obj) === {}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
util.formatDate = function (d) {
|
||||||
|
function pad(n) {
|
||||||
|
return n < 10 ? '0' + n : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function padMilliseconds(n) {
|
||||||
|
if (n < 10) {
|
||||||
|
return '00' + n;
|
||||||
|
} else if (n < 100) {
|
||||||
|
return '0' + n;
|
||||||
|
} else {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d.getFullYear() + '-' +
|
||||||
|
pad(d.getMonth() + 1) + '-' +
|
||||||
|
pad(d.getDate()) + ' ' +
|
||||||
|
pad(d.getHours()) + ':' +
|
||||||
|
pad(d.getMinutes()) + ':' +
|
||||||
|
pad(d.getSeconds()) + '.' +
|
||||||
|
padMilliseconds(d.getMilliseconds());
|
||||||
|
};
|
||||||
|
|
||||||
|
util.searchObjDate = function (o) {
|
||||||
|
if (util.check.isObject(o) || util.check.isArray(o)) {
|
||||||
|
util.each(o, function (a, b) {
|
||||||
|
if (util.check.isObject(a) || util.check.isArray(a)) {
|
||||||
|
util.searchObjDate(o[b]);
|
||||||
|
} else {
|
||||||
|
if (util.check.isDate(a)) {
|
||||||
|
o[b] = util.formatDate(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
util.check = {
|
||||||
|
isUndefined: function (obj) {
|
||||||
|
return obj === void 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
isObject: function (obj) {
|
||||||
|
return (toString.call(obj) === '[object Object]') && (obj !== null);
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmptyObject: function (obj) {
|
||||||
|
if (util.check.isObject(obj)) {
|
||||||
|
for (let key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isArray: function (obj) {
|
||||||
|
return toString.call(obj) === '[object Array]';
|
||||||
|
},
|
||||||
|
|
||||||
|
isString: function (obj) {
|
||||||
|
return toString.call(obj) === '[object String]';
|
||||||
|
},
|
||||||
|
|
||||||
|
isDate: function (obj) {
|
||||||
|
return toString.call(obj) === '[object Date]';
|
||||||
|
},
|
||||||
|
|
||||||
|
isNumber: function (obj) {
|
||||||
|
return toString.call(obj) === '[object Number]';
|
||||||
|
},
|
||||||
|
|
||||||
|
isBoolean: function (obj) {
|
||||||
|
return toString.call(obj) === '[object Boolean]';
|
||||||
|
},
|
||||||
|
|
||||||
|
isJSONString: function (str) {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
util.properties = {
|
||||||
|
_strip: function (prop) {
|
||||||
|
if (!util.check.isObject(prop)) {
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
util.each(prop, function (v, k) {
|
||||||
|
if (!(util.check.isString(v) || util.check.isNumber(v) || util.check.isDate(v) || util.check.isBoolean(v) || util.check.isArray(v))) {
|
||||||
|
util.log('您的数据-', k, v, '-格式不满足要求,我们已经将其删除. 属性值只支持 String, Number, Date, Boolean, Array');
|
||||||
|
delete prop[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prop;
|
||||||
|
},
|
||||||
|
|
||||||
|
_checkPropertiesKey: function (obj) {
|
||||||
|
let flag = true;
|
||||||
|
util.each(obj, (v, k) => {
|
||||||
|
if (!KEY_NAME_MATCH_REGEX.test(k)) {
|
||||||
|
util.log('不合法的 KEY 值: ' + k);
|
||||||
|
flag = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flag;
|
||||||
|
},
|
||||||
|
|
||||||
|
name: function (s) {
|
||||||
|
if (!util.check.isString(s) || !KEY_NAME_MATCH_REGEX.test(s)) {
|
||||||
|
util.log('请检查参数格式, 事件或属性名称必须是英文字母、 \'util\'或\'#\' 开头, 包含字母和数字的字符串: ' + s);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
properties: function (p) {
|
||||||
|
this._strip(p);
|
||||||
|
if (p) {
|
||||||
|
if (util.check.isObject(p)) {
|
||||||
|
if (this._checkPropertiesKey(p)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
util.log('请检查参数格式, properties 的 key 只能以字母或\'#\'开头,包含数字、字母和下划线 util');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
util.log('properties 可以没有,但有的话必须是对象');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
propertiesMust: function (p) {
|
||||||
|
this._strip(p);
|
||||||
|
if (p === undefined || !util.check.isObject(p) || util.check.isEmptyObject(p)) {
|
||||||
|
util.log('properties 必须是对象且有值');
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (this._checkPropertiesKey(p)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
util.log('请检查参数格式, properties 的 key 只能以字母或\'#\'开头,包含数字、字母和下划线 util');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
userId: function (id) {
|
||||||
|
if (util.check.isString(id) && /^.+$/.test(id)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
util.log('用户 id 必须是不能为空');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const DEFAULT_PROPERTIES = {
|
||||||
|
"#lib": "node",
|
||||||
|
"#lib_version": util.version,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部函数,创建 ta 实例
|
||||||
|
*/
|
||||||
|
let _createClient = function (consumer) {
|
||||||
|
let ta: any = {};
|
||||||
|
ta.consumer = consumer;
|
||||||
|
ta.superProperties = {};
|
||||||
|
|
||||||
|
ta._basicCheck = function (params) {
|
||||||
|
if (!util.check.isObject(params)) {
|
||||||
|
return new Error("params for track must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!util.properties.userId(params.accountId) && !util.properties.userId(params.distinctId)) {
|
||||||
|
return new Error("account ID and distinct ID cannot be empty at the same time");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
ta.track = function (params, skipLocalCheck = false) {
|
||||||
|
let callback = params.callback || function () {
|
||||||
|
};
|
||||||
|
if (!ta._trackCheck(params, skipLocalCheck, callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ta._sendRequest("track", params, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
ta._trackCheck = function (params, skipLocalCheck, callback) {
|
||||||
|
let err = this._basicCheck(params);
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!util.properties.name(params.event)) {
|
||||||
|
callback(new Error("invalid event name"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!skipLocalCheck && !util.properties.properties(params.properties)) {
|
||||||
|
callback(new Error("invalid properties"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ta.userSet = function (params) {
|
||||||
|
let callback = params.callback || function () {
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = this._basicCheck(params);
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!util.properties.propertiesMust(params.properties)) {
|
||||||
|
callback(new Error("invalid properties"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ta._sendRequest("user_set", params, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
ta.userSetOnce = function (params) {
|
||||||
|
let callback = params.callback || function () {
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = this._basicCheck(params);
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!util.properties.propertiesMust(params.properties)) {
|
||||||
|
callback(new Error("invalid properties"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ta._sendRequest("user_setOnce", params, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
ta.userAdd = function (params) {
|
||||||
|
let callback = params.callback || function () {
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = this._basicCheck(params);
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!util.properties.propertiesMust(params.properties)) {
|
||||||
|
callback(new Error("invalid properties"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ta._sendRequest("user_add", params, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
ta.setDynamicSuperProperties = function (getDynamicProperties, callback) {
|
||||||
|
let err;
|
||||||
|
if (typeof getDynamicProperties === "function") {
|
||||||
|
if (util.properties.properties(getDynamicProperties())) {
|
||||||
|
ta.getDynamicProperties = getDynamicProperties;
|
||||||
|
} else {
|
||||||
|
err = new Error("Invalid return type of getDynamicProperties");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = new Error("getDynamicProperties must be a funciton");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback && err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内部函数,组织数据格式,并发送给对应的 consumer 去处理
|
||||||
|
ta._sendRequest = function (type, eventData, callback) {
|
||||||
|
let time = util.check.isUndefined(eventData.time) || !util.check.isDate(eventData.time)
|
||||||
|
? new Date()
|
||||||
|
: eventData.time;
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"#type": type,
|
||||||
|
"#time": util.formatDate(time),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (eventData.distinctId) {
|
||||||
|
data["#distinct_id"] = eventData.distinctId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.accountId) {
|
||||||
|
data["#account_id"] = eventData.accountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.ip) {
|
||||||
|
data["#ip"] = eventData.ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.event) {
|
||||||
|
data["#event_name"] = eventData.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.eventId) {
|
||||||
|
data["#event_id"] = eventData.eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.firstCheckId) {
|
||||||
|
data["#first_check_id"] = eventData.firstCheckId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.uuid) {
|
||||||
|
data["#uuid"] = eventData.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.appId) {
|
||||||
|
data["#app_id"] = eventData.appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "track" ||
|
||||||
|
type === "track_update" ||
|
||||||
|
type === "track_overwrite") {
|
||||||
|
data["properties"] = { ...DEFAULT_PROPERTIES, ...ta.superProperties, ...(ta.getDynamicProperties ? ta.getDynamicProperties() : {}) };
|
||||||
|
} else {
|
||||||
|
data["properties"] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
util.check.isObject(eventData.properties) &&
|
||||||
|
!util.check.isEmptyObject(eventData.properties)
|
||||||
|
) {
|
||||||
|
data['properties'] = { ...data['properties'], ...eventData.properties || {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
util.searchObjDate(data);
|
||||||
|
|
||||||
|
this.consumer.add(data, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initTaLoggingMode() {
|
||||||
|
return _createClient({
|
||||||
|
add: (msg: string) => {
|
||||||
|
taLogger.info(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import { request37CheckChat, request37GetWord, request37Post } from "../pubUtils
|
|||||||
import { GuildModel } from "../db/Guild";
|
import { GuildModel } from "../db/Guild";
|
||||||
import { getRoleOnlineInfo, updateUserInfo } from "./redisService";
|
import { getRoleOnlineInfo, updateUserInfo } from "./redisService";
|
||||||
import { Application, pinus } from "pinus";
|
import { Application, pinus } from "pinus";
|
||||||
import { getGuildChannelSid } from "./chatService";
|
|
||||||
import { getRandSingleEelm, parseGoodStr, readWordTxt, resResult, writeWordTxt } from "../pubUtils/util";
|
import { getRandSingleEelm, parseGoodStr, readWordTxt, resResult, writeWordTxt } from "../pubUtils/util";
|
||||||
const ThinkingAnalytics = require("thinkingdata-node");
|
const ThinkingAnalytics = require("thinkingdata-node");
|
||||||
import Trie from '../pubUtils/trie';
|
import Trie from '../pubUtils/trie';
|
||||||
@@ -21,6 +20,7 @@ import { SurveyModel } from "../db/Survery";
|
|||||||
import { pushGuildInfoUpdate } from "./guildService";
|
import { pushGuildInfoUpdate } from "./guildService";
|
||||||
import { sendMessageToUserWithSuc } from "./pushService";
|
import { sendMessageToUserWithSuc } from "./pushService";
|
||||||
import { GuildLeader } from "../domain/rank";
|
import { GuildLeader } from "../domain/rank";
|
||||||
|
import { initTaLoggingMode } from "./sdk/ta";
|
||||||
|
|
||||||
// 检查私聊是否合法
|
// 检查私聊是否合法
|
||||||
|
|
||||||
@@ -171,9 +171,7 @@ export function connectThinkingData(app: Application) {
|
|||||||
} else if (THINKING_DATA_MODE == THINKING_DATA_MODE_LIST.BATCH) {
|
} else if (THINKING_DATA_MODE == THINKING_DATA_MODE_LIST.BATCH) {
|
||||||
ta = ThinkingAnalytics.initWithBatchMode(SDK_TA_CONST.APPID, SDK_TA_CONST.SERVER_URL);
|
ta = ThinkingAnalytics.initWithBatchMode(SDK_TA_CONST.APPID, SDK_TA_CONST.SERVER_URL);
|
||||||
} else if (THINKING_DATA_MODE == THINKING_DATA_MODE_LIST.LOGGING) {
|
} else if (THINKING_DATA_MODE == THINKING_DATA_MODE_LIST.LOGGING) {
|
||||||
ta = ThinkingAnalytics.initWithLoggingMode(SDK_TA_CONST.LOG_PATH, {
|
ta = initTaLoggingMode();
|
||||||
pm2: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
ta.setDynamicSuperProperties(() => {
|
ta.setDynamicSuperProperties(() => {
|
||||||
return {
|
return {
|
||||||
@@ -245,8 +243,8 @@ export async function reportCreateRoleEventToTa(role: RoleType, ip: string) {
|
|||||||
export function taflush() {
|
export function taflush() {
|
||||||
let ta = pinus.app.get('ta');
|
let ta = pinus.app.get('ta');
|
||||||
if(!ta) return;
|
if(!ta) return;
|
||||||
ta.flush();
|
if(ta.flush) ta.flush();
|
||||||
ta.close();
|
if(ta.close) ta.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch37Words() {
|
export async function fetch37Words() {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { getLogger } from 'pinus-logger';
|
import { getLogger, Logger } from 'pinus-logger';
|
||||||
|
|
||||||
|
let logger: Logger;
|
||||||
|
let infologger: Logger;
|
||||||
|
let errlogger: Logger;
|
||||||
|
let taLogger: Logger;
|
||||||
|
|
||||||
|
export function loadLogger(sid: string) {
|
||||||
|
logger = getLogger();
|
||||||
|
infologger = getLogger('info', sid);
|
||||||
|
errlogger = getLogger('err', sid);
|
||||||
|
taLogger = getLogger('ta');
|
||||||
|
}
|
||||||
|
|
||||||
let logger = getLogger();
|
|
||||||
let infologger = getLogger('info')
|
|
||||||
let errlogger = getLogger('err');
|
|
||||||
export {
|
export {
|
||||||
logger,
|
logger,
|
||||||
infologger,
|
infologger,
|
||||||
errlogger
|
errlogger,
|
||||||
|
taLogger
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
'appenders': {
|
'appenders': {
|
||||||
'console': {
|
'console': {
|
||||||
'type': 'console'
|
'type': 'console',
|
||||||
|
'prefix': '${opts:serverId}'
|
||||||
},
|
},
|
||||||
'con-log': {
|
'con-log': {
|
||||||
'type': 'file',
|
'type': 'file',
|
||||||
@@ -102,6 +103,17 @@ module.exports = {
|
|||||||
'type': 'basic'
|
'type': 'basic'
|
||||||
},
|
},
|
||||||
'backups': 5
|
'backups': 5
|
||||||
|
},
|
||||||
|
'ta': {
|
||||||
|
'type': 'file',
|
||||||
|
'filename': '/zyz_logs/ta/log',
|
||||||
|
'pattern': 'yyyy-MM-dd-hh',
|
||||||
|
'alwaysIncludePattern': true,
|
||||||
|
'layout': {
|
||||||
|
'type': 'pattern',
|
||||||
|
'pattern': '%m',
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -153,11 +165,12 @@ module.exports = {
|
|||||||
'err': {
|
'err': {
|
||||||
'appenders': ['console', 'error'],
|
'appenders': ['console', 'error'],
|
||||||
'level': 'debug'
|
'level': 'debug'
|
||||||
|
},
|
||||||
|
'ta': {
|
||||||
|
'appenders': ['console', 'ta'],
|
||||||
|
'level': 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'prefix': '${opts:serverId} ',
|
|
||||||
'replaceConsole': true,
|
'replaceConsole': true,
|
||||||
'lineDebug': false,
|
'lineDebug': false,
|
||||||
'errorStack': true
|
'errorStack': true
|
||||||
|
|||||||
Reference in New Issue
Block a user