ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

logger.ts 5.6 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { context, trace } from '@opentelemetry/api'
  2. import { omit } from '@peertube/peertube-core-utils'
  3. import { stat } from 'fs/promises'
  4. import { join } from 'path'
  5. import { format as sqlFormat } from 'sql-formatter'
  6. import { createLogger, format, transports } from 'winston'
  7. import { FileTransportOptions } from 'winston/lib/winston/transports'
  8. import { CONFIG } from '../initializers/config.js'
  9. import { LOG_FILENAME } from '../initializers/constants.js'
  10. const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
  11. const consoleLoggerFormat = format.printf(info => {
  12. let additionalInfos = JSON.stringify(getAdditionalInfo(info), removeCyclicValues(), 2)
  13. if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = ''
  14. else additionalInfos = ' ' + additionalInfos
  15. if (info.sql) {
  16. if (CONFIG.LOG.PRETTIFY_SQL) {
  17. additionalInfos += '\n' + sqlFormat(info.sql, {
  18. language: 'sql',
  19. tabWidth: 2
  20. })
  21. } else {
  22. additionalInfos += ' - ' + info.sql
  23. }
  24. }
  25. return `[${info.label}] ${info.timestamp} ${info.level}: ${info.message}${additionalInfos}`
  26. })
  27. const jsonLoggerFormat = format.printf(info => {
  28. return JSON.stringify(info, removeCyclicValues())
  29. })
  30. const timestampFormatter = format.timestamp({
  31. format: 'YYYY-MM-DD HH:mm:ss.SSS'
  32. })
  33. const labelFormatter = (suffix?: string) => {
  34. return format.label({
  35. label: suffix ? `${label} ${suffix}` : label
  36. })
  37. }
  38. const fileLoggerOptions: FileTransportOptions = {
  39. filename: join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME),
  40. handleExceptions: true,
  41. format: format.combine(
  42. format.timestamp(),
  43. jsonLoggerFormat
  44. )
  45. }
  46. if (CONFIG.LOG.ROTATION.ENABLED) {
  47. fileLoggerOptions.maxsize = CONFIG.LOG.ROTATION.MAX_FILE_SIZE
  48. fileLoggerOptions.maxFiles = CONFIG.LOG.ROTATION.MAX_FILES
  49. }
  50. function buildLogger (labelSuffix?: string) {
  51. return createLogger({
  52. level: process.env.LOGGER_LEVEL ?? CONFIG.LOG.LEVEL,
  53. defaultMeta: {
  54. get traceId () { return trace.getSpanContext(context.active())?.traceId },
  55. get spanId () { return trace.getSpanContext(context.active())?.spanId },
  56. get traceFlags () { return trace.getSpanContext(context.active())?.traceFlags }
  57. },
  58. format: format.combine(
  59. labelFormatter(labelSuffix),
  60. format.splat()
  61. ),
  62. transports: [
  63. new transports.File(fileLoggerOptions),
  64. new transports.Console({
  65. handleExceptions: true,
  66. format: format.combine(
  67. timestampFormatter,
  68. format.colorize(),
  69. consoleLoggerFormat
  70. )
  71. })
  72. ],
  73. exitOnError: true
  74. })
  75. }
  76. const logger = buildLogger()
  77. // ---------------------------------------------------------------------------
  78. function bunyanLogFactory (level: string) {
  79. return function (...params: any[]) {
  80. let meta = null
  81. let args = [].concat(params)
  82. if (arguments[0] instanceof Error) {
  83. meta = arguments[0].toString()
  84. args = Array.prototype.slice.call(arguments, 1)
  85. args.push(meta)
  86. } else if (typeof (args[0]) !== 'string') {
  87. meta = arguments[0]
  88. args = Array.prototype.slice.call(arguments, 1)
  89. args.push(meta)
  90. }
  91. logger[level].apply(logger, args)
  92. }
  93. }
  94. const bunyanLogger = {
  95. level: () => { },
  96. trace: bunyanLogFactory('debug'),
  97. debug: bunyanLogFactory('debug'),
  98. verbose: bunyanLogFactory('debug'),
  99. info: bunyanLogFactory('info'),
  100. warn: bunyanLogFactory('warn'),
  101. error: bunyanLogFactory('error'),
  102. fatal: bunyanLogFactory('error')
  103. }
  104. // ---------------------------------------------------------------------------
  105. type LoggerTags = { tags: (string | number)[] }
  106. type LoggerTagsFn = (...tags: (string | number)[]) => LoggerTags
  107. function loggerTagsFactory (...defaultTags: (string | number)[]): LoggerTagsFn {
  108. return (...tags: (string | number)[]) => {
  109. return { tags: defaultTags.concat(tags) }
  110. }
  111. }
  112. // ---------------------------------------------------------------------------
  113. async function mtimeSortFilesDesc (files: string[], basePath: string) {
  114. const promises = []
  115. const out: { file: string, mtime: number }[] = []
  116. for (const file of files) {
  117. const p = stat(basePath + '/' + file)
  118. .then(stats => {
  119. if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
  120. })
  121. promises.push(p)
  122. }
  123. await Promise.all(promises)
  124. out.sort((a, b) => b.mtime - a.mtime)
  125. return out
  126. }
  127. // ---------------------------------------------------------------------------
  128. export {
  129. buildLogger, bunyanLogger, consoleLoggerFormat,
  130. jsonLoggerFormat, labelFormatter, logger,
  131. loggerTagsFactory, mtimeSortFilesDesc, timestampFormatter, type LoggerTags, type LoggerTagsFn
  132. }
  133. // ---------------------------------------------------------------------------
  134. function removeCyclicValues () {
  135. const seen = new WeakSet()
  136. // Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#Examples
  137. return (key: string, value: any) => {
  138. if (key === 'cert') return 'Replaced by the logger to avoid large log message'
  139. if (typeof value === 'object' && value !== null) {
  140. if (seen.has(value)) return
  141. seen.add(value)
  142. }
  143. if (value instanceof Set) {
  144. return Array.from(value)
  145. }
  146. if (value instanceof Map) {
  147. return Array.from(value.entries())
  148. }
  149. if (value instanceof Error) {
  150. const error = {}
  151. Object.getOwnPropertyNames(value).forEach(key => { error[key] = value[key] })
  152. return error
  153. }
  154. return value
  155. }
  156. }
  157. function getAdditionalInfo (info: any) {
  158. const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ]
  159. return omit(info, toOmit)
  160. }