gcp-manager.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. const bunyan = require('bunyan')
  2. /**
  3. * When we copy log entry fields, omit some bunyan core fields that are not
  4. * interesting, that have a special meaning in GCP, or that we will process
  5. * separately.
  6. */
  7. const ENTRY_FIELDS_TO_OMIT = [
  8. 'level',
  9. 'name',
  10. 'hostname',
  11. 'v',
  12. 'pid',
  13. 'msg',
  14. 'err',
  15. 'error',
  16. 'req',
  17. 'res',
  18. ]
  19. /**
  20. * Convert a bunyan log entry to a format that GCP understands
  21. */
  22. function convertLogEntry(entry) {
  23. const gcpEntry = omit(entry, ENTRY_FIELDS_TO_OMIT)
  24. // Error information. In GCP, the stack trace goes in the message property.
  25. // This enables the error reporting feature.
  26. const err = entry.err || entry.error
  27. if (err) {
  28. if (err.info) {
  29. Object.assign(gcpEntry, err.info)
  30. }
  31. if (err.code) {
  32. gcpEntry.code = err.code
  33. }
  34. if (err.signal) {
  35. gcpEntry.signal = err.signal
  36. }
  37. const stack = err.stack
  38. if (stack && stack !== '(no stack)') {
  39. gcpEntry.message = stack
  40. } else if (err.message) {
  41. gcpEntry.message = err.message
  42. }
  43. if (entry.name) {
  44. gcpEntry.serviceContext = { service: entry.name }
  45. }
  46. }
  47. // Log message
  48. if (entry.msg) {
  49. if (gcpEntry.message) {
  50. // A message has already been extracted from the error. Keep the extra
  51. // message in the msg property.
  52. gcpEntry.msg = entry.msg
  53. } else {
  54. gcpEntry.message = entry.msg
  55. }
  56. }
  57. // Severity
  58. if (entry.level) {
  59. gcpEntry.severity = bunyan.nameFromLevel[entry.level]
  60. }
  61. // HTTP request information
  62. if (entry.req || entry.res || entry.responseTimeMs) {
  63. const httpRequest = {}
  64. if (entry.req) {
  65. const req = entry.req
  66. httpRequest.requestMethod = req.method
  67. httpRequest.requestUrl = req.url
  68. httpRequest.remoteIp = req.remoteAddress
  69. if (req.headers) {
  70. if (req.headers['content-length']) {
  71. httpRequest.requestSize = parseInt(req.headers['content-length'], 10)
  72. }
  73. httpRequest.userAgent = req.headers['user-agent']
  74. httpRequest.referer = req.headers.referer
  75. }
  76. }
  77. if (entry.res) {
  78. const res = entry.res
  79. httpRequest.status = res.statusCode
  80. if (res.headers && res.headers['content-length']) {
  81. if (res.headers['content-length']) {
  82. httpRequest.responseSize = parseInt(res.headers['content-length'], 10)
  83. }
  84. }
  85. }
  86. if (entry.responseTimeMs) {
  87. const responseTimeSec = entry.responseTimeMs / 1000
  88. httpRequest.latency = `${responseTimeSec}s`
  89. }
  90. gcpEntry.httpRequest = httpRequest
  91. }
  92. // Labels are indexed in GCP. We copy the project, doc and user ids to labels to enable fast filtering
  93. const projectId =
  94. gcpEntry.projectId ||
  95. gcpEntry.project_id ||
  96. (entry.req && entry.req.projectId)
  97. const userId =
  98. gcpEntry.userId || gcpEntry.user_id || (entry.req && entry.req.userId)
  99. const docId =
  100. gcpEntry.docId || gcpEntry.doc_id || (entry.req && entry.req.docId)
  101. if (projectId || userId || docId) {
  102. const labels = {}
  103. if (projectId) {
  104. labels.projectId = projectId
  105. }
  106. if (userId) {
  107. labels.userId = userId
  108. }
  109. if (docId) {
  110. labels.docId = docId
  111. }
  112. gcpEntry['logging.googleapis.com/labels'] = labels
  113. }
  114. return gcpEntry
  115. }
  116. function omit(obj, excludedFields) {
  117. return Object.fromEntries(
  118. Object.entries(obj).filter(([key]) => !excludedFields.includes(key))
  119. )
  120. }
  121. module.exports = { convertLogEntry }