azify-logger 1.0.26 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/register.js CHANGED
@@ -4,16 +4,144 @@ if (process.env.AZIFY_LOGGER_DISABLE === '1') {
4
4
  try {
5
5
  const bunyan = require('bunyan')
6
6
  const createBunyanStream = require('./streams/bunyan')
7
+ const { createHttpLoggerTransport } = require('./streams/httpQueue')
7
8
  const { getRequestContext } = require('./store')
9
+ const { randomUUID, randomBytes } = require('crypto')
10
+ const { performance } = require('perf_hooks')
11
+ const { URL } = require('url')
12
+ const os = require('os')
13
+
14
+ const { shouldSample, markSource, HTTP_CLIENT_MODE } = require('./sampling')
15
+
16
+ const serviceName = process.env.APP_NAME
17
+ const environment = process.env.NODE_ENV
18
+ const loggerUrlFromEnv = process.env.AZIFY_LOGGER_URL
19
+
20
+ let loggerEndpoint
21
+ try {
22
+ loggerEndpoint = new URL(loggerUrlFromEnv)
23
+ } catch (_) {
24
+ loggerEndpoint = new URL('http://localhost:3001/log')
25
+ }
26
+
27
+ const loggerUrlString = loggerEndpoint.toString()
28
+ const transport = createHttpLoggerTransport(loggerUrlString)
29
+
30
+ const flushTransport = () => transport.flush().catch(() => {})
31
+ process.once('beforeExit', flushTransport)
32
+ process.once('exit', flushTransport)
33
+
34
+ const normalizedLoggerOrigin = `${loggerEndpoint.protocol}//${loggerEndpoint.host}`
35
+ const normalizedLoggerPath = normalizePath(loggerEndpoint.pathname || '/')
36
+
37
+ const SENSITIVE_HEADER_KEYS = new Set([
38
+ 'authorization',
39
+ 'cookie',
40
+ 'set-cookie',
41
+ 'x-api-key',
42
+ 'x-auth-token',
43
+ 'x-access-token',
44
+ 'proxy-authorization'
45
+ ])
46
+
47
+ function sanitizeHeaderEntries(headers) {
48
+ const sanitized = {}
49
+ if (!headers) {
50
+ return sanitized
51
+ }
52
+
53
+ const assignValue = (key, value) => {
54
+ if (key == null) {
55
+ return
56
+ }
57
+ const lower = String(key).toLowerCase()
58
+ if (!lower) {
59
+ return
60
+ }
61
+ sanitized[lower] = SENSITIVE_HEADER_KEYS.has(lower) ? '***' : String(value)
62
+ }
63
+
64
+ if (typeof headers.forEach === 'function') {
65
+ headers.forEach((value, key) => assignValue(key, value))
66
+ } else if (Array.isArray(headers)) {
67
+ headers.forEach(([key, value]) => assignValue(key, value))
68
+ } else if (typeof headers === 'object') {
69
+ Object.entries(headers).forEach(([key, value]) => {
70
+ if (Array.isArray(value)) {
71
+ value.forEach((inner) => assignValue(key, inner))
72
+ } else if (value != null) {
73
+ assignValue(key, value)
74
+ }
75
+ })
76
+ }
77
+
78
+ return sanitized
79
+ }
80
+
81
+ function sendOutboundLog(level, message, meta) {
82
+ const source = meta && meta.__source
83
+ if (!shouldSample(level, source)) {
84
+ return
85
+ }
86
+
87
+ const payload = {
88
+ level,
89
+ message,
90
+ meta: {
91
+ ...meta,
92
+ service: {
93
+ name: serviceName,
94
+ version: (meta && meta.service && meta.service.version) || '1.0.0'
95
+ },
96
+ environment,
97
+ timestamp: new Date().toISOString(),
98
+ hostname: os.hostname()
99
+ }
100
+ }
101
+
102
+ transport.enqueue(payload, {
103
+ 'content-type': 'application/json'
104
+ })
105
+ }
106
+
107
+ function normalizePath(path) {
108
+ if (!path) {
109
+ return '/'
110
+ }
111
+ const trimmed = String(path).replace(/\/+$/, '')
112
+ return trimmed === '' ? '/' : trimmed
113
+ }
114
+
115
+ function isLoggerApiCall(meta) {
116
+ if (!meta || !meta.url) {
117
+ return false
118
+ }
119
+
120
+ const candidate = meta.url
121
+
122
+ try {
123
+ const target = new URL(candidate, normalizedLoggerOrigin)
124
+ const targetOrigin = `${target.protocol}//${target.host}`
125
+ const targetPath = normalizePath(target.pathname)
126
+ return targetOrigin === normalizedLoggerOrigin && targetPath === normalizedLoggerPath
127
+ } catch (_) {
128
+ if (typeof candidate === 'string') {
129
+ const relativePath = normalizePath(candidate)
130
+ return relativePath === normalizedLoggerPath
131
+ }
132
+ }
133
+
134
+ return false
135
+ }
8
136
 
9
137
  const originalCreate = bunyan.createLogger
10
138
  bunyan.createLogger = function patchedCreateLogger(options) {
11
139
  const logger = originalCreate.call(bunyan, options)
12
140
  try {
13
141
  const level = process.env.AZIFY_LOG_LEVEL || (options && options.level) || 'info'
14
- const loggerUrl = process.env.AZIFY_LOGGER_URL || 'http://localhost:3001'
15
- const serviceName = process.env.APP_NAME || (options && options.name) || 'app'
16
- const environment = process.env.NODE_ENV || 'development'
142
+ const loggerUrl = process.env.AZIFY_LOGGER_URL
143
+ const serviceName = process.env.APP_NAME || (options && options.name)
144
+ const environment = process.env.NODE_ENV
17
145
 
18
146
  logger.addStream({
19
147
  level,
@@ -350,33 +478,170 @@ try {
350
478
 
351
479
  try {
352
480
  const axios = require('axios')
353
- if (axios && axios.create) {
354
- const originalCreate = axios.create
355
- axios.create = function(config) {
356
- const instance = originalCreate.call(this, config)
357
-
358
- const originalRequest = instance.interceptors.request.use
359
- instance.interceptors.request.use = function(fulfilled, rejected) {
360
- return originalRequest.call(this, (config) => {
361
- const { getRequestContext } = require('./store')
481
+ if (axios) {
482
+ const { getRequestContext, runWithRequestContext } = require('./store')
483
+
484
+ const buildUrl = (config) => {
485
+ const url = config.url || ''
486
+ if (/^https?:\/\//i.test(url)) {
487
+ return url
488
+ }
489
+
490
+ if (config.baseURL) {
491
+ try {
492
+ return new URL(url, config.baseURL).toString()
493
+ } catch (_) {
494
+ const base = String(config.baseURL).replace(/\/$/, '')
495
+ const path = String(url).replace(/^\//, '')
496
+ return `${base}/${path}`
497
+ }
498
+ }
499
+
500
+ return url
501
+ }
502
+
503
+ const patchInstance = (instance) => {
504
+ if (instance.__azifyLoggerPatched) {
505
+ return instance
506
+ }
507
+ instance.__azifyLoggerPatched = true
508
+
509
+ if (HTTP_CLIENT_MODE === 'off') {
510
+ return instance
511
+ }
512
+
513
+ instance.interceptors.request.use(
514
+ (config) => {
515
+ const method = (config.method || 'get').toUpperCase()
516
+ const url = buildUrl(config)
517
+ const testMeta = { url }
518
+ if (isLoggerApiCall(testMeta)) {
519
+ return config
520
+ }
521
+
362
522
  const ctx = getRequestContext()
363
-
364
- if (ctx && ctx.traceId) {
365
- config.headers = {
366
- ...config.headers,
367
- 'X-Trace-ID': ctx.traceId,
368
- 'X-Span-ID': ctx.spanId || '',
369
- 'X-Parent-Span-ID': ctx.parentSpanId || '',
370
- 'X-Request-ID': ctx.requestId || require('uuid').v4()
523
+ const traceId = (ctx?.traceId) || randomUUID()
524
+ const parentSpanId = (ctx?.spanId) || null
525
+ const requestId = (ctx?.requestId) || randomUUID()
526
+ const spanId = randomBytes(8).toString('hex')
527
+
528
+ config.headers = {
529
+ ...(config.headers || {}),
530
+ 'x-trace-id': traceId,
531
+ 'x-span-id': spanId,
532
+ 'x-parent-span-id': parentSpanId || '',
533
+ 'x-request-id': requestId
534
+ }
535
+
536
+ const requestMeta = {
537
+ traceId,
538
+ spanId,
539
+ parentSpanId,
540
+ requestId,
541
+ method,
542
+ url,
543
+ headers: config.headers
544
+ }
545
+
546
+ markSource(requestMeta, 'http-client')
547
+ config.__azifyLogger = {
548
+ meta: requestMeta,
549
+ start: performance.now()
550
+ }
551
+ const childCtx = { traceId, spanId, parentSpanId, requestId }
552
+ config.__azifyChildCtx = childCtx
553
+
554
+ if (HTTP_CLIENT_MODE === 'all') {
555
+ sendOutboundLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
556
+ }
557
+
558
+ return config
559
+ },
560
+ (error) => Promise.reject(error)
561
+ )
562
+
563
+ instance.interceptors.response.use(
564
+ (response) => {
565
+ const url = buildUrl(response.config)
566
+ const testMeta = { url }
567
+ if (isLoggerApiCall(testMeta)) {
568
+ return response
569
+ }
570
+
571
+ const marker = response.config?.__azifyLogger
572
+ const childCtx = response.config?.__azifyChildCtx
573
+
574
+ if (marker && marker.meta) {
575
+ const duration = Number((performance.now() - marker.start).toFixed(2))
576
+ const shouldLogResponse =
577
+ HTTP_CLIENT_MODE === 'all' ||
578
+ (HTTP_CLIENT_MODE === 'errors' && response.status >= 400)
579
+
580
+ if (shouldLogResponse) {
581
+ const meta = {
582
+ ...marker.meta,
583
+ statusCode: response.status,
584
+ responseTimeMs: duration,
585
+ responseHeaders: response.headers,
586
+ responseBody: response.data
587
+ }
588
+
589
+ markSource(meta, 'http-client')
590
+ const message = `[RESPONSE] ${meta.method} ${meta.url} ${response.status} ${duration}ms`
591
+ const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
592
+
593
+ sendOutboundLog(level, message, meta)
371
594
  }
372
595
  }
373
-
374
- return fulfilled ? fulfilled(config) : config
375
- }, rejected)
376
- }
377
-
596
+
597
+ return response
598
+ },
599
+ (error) => {
600
+ const config = error?.config
601
+ if (config) {
602
+ const url = buildUrl(config)
603
+ const testMeta = { url }
604
+ if (isLoggerApiCall(testMeta)) {
605
+ return Promise.reject(error)
606
+ }
607
+ }
608
+
609
+ const marker = config?.__azifyLogger
610
+ const childCtx = config?.__azifyChildCtx
611
+
612
+ if (marker && marker.meta) {
613
+ const duration = Number((performance.now() - marker.start).toFixed(2))
614
+ const meta = {
615
+ ...marker.meta,
616
+ responseTimeMs: duration,
617
+ error: {
618
+ name: error?.name || 'Error',
619
+ message: error?.message || String(error),
620
+ stack: error?.stack
621
+ }
622
+ }
623
+ markSource(meta, 'http-client')
624
+ const message = `[ERROR] ${meta.method} ${meta.url}`
625
+
626
+ sendOutboundLog('error', message, meta)
627
+ }
628
+
629
+ return Promise.reject(error)
630
+ }
631
+ )
632
+
378
633
  return instance
379
634
  }
635
+
636
+ patchInstance(axios)
637
+
638
+ if (axios.create) {
639
+ const originalCreate = axios.create
640
+ axios.create = function(config) {
641
+ const instance = originalCreate.call(this, config)
642
+ return patchInstance(instance)
643
+ }
644
+ }
380
645
  }
381
646
  } catch (_) {}
382
647
 
@@ -387,63 +652,9 @@ try {
387
652
  g.__azifyLoggerFetchPatched = true
388
653
 
389
654
  const { getRequestContext, runWithRequestContext } = require('./store')
390
- const { randomUUID, randomBytes } = require('crypto')
391
- const { performance } = require('perf_hooks')
392
- const axios = require('axios')
393
- const os = require('os')
394
-
395
- const serviceName = process.env.APP_NAME || 'app'
396
- const loggerUrl = process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log'
397
- const environment = process.env.NODE_ENV || 'development'
398
-
399
- async function sendLog(level, message, meta) {
400
- const payload = {
401
- level,
402
- message,
403
- meta: {
404
- ...meta,
405
- service: {
406
- name: serviceName,
407
- version: (meta && meta.service && meta.service.version) || '1.0.0'
408
- },
409
- environment,
410
- timestamp: new Date().toISOString(),
411
- hostname: os.hostname()
412
- }
413
- }
414
-
415
- try {
416
- await axios.post(loggerUrl, payload, { timeout: 5000 })
417
- } catch (error) {
418
- console.error('Erro ao enviar log:', error && error.message ? error.message : error)
419
- }
420
- }
421
655
 
422
656
  const originalFetch = globalThis.fetch.bind(globalThis)
423
657
 
424
- function sanitizeHeaders(headers) {
425
- const SENSITIVE_HEADER_KEYS = new Set([
426
- 'authorization',
427
- 'cookie',
428
- 'set-cookie',
429
- 'x-api-key',
430
- 'x-auth-token',
431
- 'x-access-token',
432
- 'proxy-authorization'
433
- ])
434
-
435
- const sanitized = {}
436
- headers.forEach((value, key) => {
437
- const lower = key.toLowerCase()
438
- if (SENSITIVE_HEADER_KEYS.has(lower)) {
439
- sanitized[lower] = '***'
440
- } else {
441
- sanitized[lower] = value
442
- }
443
- })
444
- return sanitized
445
- }
446
-
447
658
  function ensureRequest(input, init) {
448
659
  if (typeof Request !== 'undefined' && input instanceof Request) {
449
660
  return init ? new Request(input, init) : input
@@ -452,14 +663,23 @@ try {
452
663
  }
453
664
 
454
665
  globalThis.fetch = async function patchedFetch(input, init) {
666
+ if (HTTP_CLIENT_MODE === 'off') {
667
+ return originalFetch(input, init)
668
+ }
669
+
455
670
  const request = ensureRequest(input, init)
456
671
  const method = request.method.toUpperCase()
457
672
  const url = request.url
458
673
 
674
+ const testMeta = { url }
675
+ if (isLoggerApiCall(testMeta)) {
676
+ return originalFetch(request)
677
+ }
678
+
459
679
  const ctx = getRequestContext()
460
- const traceId = (ctx && ctx.traceId) || randomUUID()
461
- const parentSpanId = (ctx && ctx.spanId) || null
462
- const requestId = (ctx && ctx.requestId) || randomUUID()
680
+ const traceId = (ctx?.traceId) || randomUUID()
681
+ const parentSpanId = (ctx?.spanId) || null
682
+ const requestId = (ctx?.requestId) || randomUUID()
463
683
  const spanId = randomBytes(8).toString('hex')
464
684
 
465
685
  request.headers.set('x-trace-id', traceId)
@@ -474,11 +694,13 @@ try {
474
694
  requestId,
475
695
  method,
476
696
  url,
477
- headers: sanitizeHeaders(request.headers)
697
+ headers: Object.fromEntries(request.headers.entries())
478
698
  }
479
699
 
480
- const start = performance.now()
481
- void sendLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
700
+ markSource(requestMeta, 'http-client')
701
+ if (HTTP_CLIENT_MODE === 'all') {
702
+ sendOutboundLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
703
+ }
482
704
 
483
705
  const childCtx = {
484
706
  traceId,
@@ -487,25 +709,67 @@ try {
487
709
  requestId
488
710
  }
489
711
 
712
+ const start = performance.now()
713
+
490
714
  try {
491
715
  const response = await runWithRequestContext(childCtx, function() {
492
716
  return originalFetch(request)
493
717
  })
494
718
 
495
719
  const duration = Number((performance.now() - start).toFixed(2))
496
- const responseMeta = {
497
- ...requestMeta,
498
- statusCode: response.status,
499
- responseTimeMs: duration
500
- }
501
-
502
- const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
503
- if (response.status >= 500) {
504
- void sendLog('error', message, responseMeta)
505
- } else if (response.status >= 400) {
506
- void sendLog('warn', message, responseMeta)
507
- } else {
508
- void sendLog('info', message, responseMeta)
720
+ const shouldLogResponse =
721
+ HTTP_CLIENT_MODE === 'all' ||
722
+ (HTTP_CLIENT_MODE === 'errors' && response.status >= 400)
723
+
724
+ if (shouldLogResponse) {
725
+ (async () => {
726
+ try {
727
+ const clonedResponse = response.clone()
728
+ const contentType = response.headers.get('content-type') || ''
729
+
730
+ let responseBody = null
731
+
732
+ if (contentType.includes('application/json') || contentType.includes('text/')) {
733
+ const bodyText = await clonedResponse.text()
734
+ if (contentType.includes('application/json')) {
735
+ try {
736
+ responseBody = JSON.parse(bodyText)
737
+ } catch (_) {
738
+ responseBody = bodyText
739
+ }
740
+ } else {
741
+ responseBody = bodyText
742
+ }
743
+ }
744
+
745
+ const responseMeta = {
746
+ ...requestMeta,
747
+ statusCode: response.status,
748
+ responseTimeMs: duration,
749
+ responseHeaders: Object.fromEntries(response.headers.entries()),
750
+ responseBody: responseBody
751
+ }
752
+
753
+ markSource(responseMeta, 'http-client')
754
+ const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
755
+
756
+ const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
757
+ sendOutboundLog(level, message, responseMeta)
758
+ } catch (_) {
759
+ const responseMeta = {
760
+ ...requestMeta,
761
+ statusCode: response.status,
762
+ responseTimeMs: duration,
763
+ responseHeaders: Object.fromEntries(response.headers.entries()),
764
+ responseBody: null
765
+ }
766
+
767
+ markSource(responseMeta, 'http-client')
768
+ const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
769
+ const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
770
+ sendOutboundLog(level, message, responseMeta)
771
+ }
772
+ })()
509
773
  }
510
774
 
511
775
  return response
@@ -515,13 +779,14 @@ try {
515
779
  ...requestMeta,
516
780
  responseTimeMs: duration
517
781
  }
782
+ markSource(errorMeta, 'http-client')
518
783
  const err = error instanceof Error ? error : new Error(String(error))
519
784
  errorMeta.error = {
520
785
  name: err.name,
521
786
  message: err.message,
522
787
  stack: err.stack
523
788
  }
524
- void sendLog('error', `[ERROR] ${method} ${url}`, errorMeta)
789
+ void sendOutboundLog('error', `[ERROR] ${method} ${url}`, errorMeta)
525
790
  throw error
526
791
  }
527
792
  }
package/sampling.js ADDED
@@ -0,0 +1,79 @@
1
+ const clampRate = (value) => {
2
+ if (value === null || value === undefined || value === '') {
3
+ return null
4
+ }
5
+ const numeric = Number(value)
6
+ if (!Number.isFinite(numeric)) {
7
+ return null
8
+ }
9
+ return Math.max(0, Math.min(1, numeric))
10
+ }
11
+
12
+ const defaultSampleRate = clampRate(process.env.AZIFY_LOGGER_SAMPLE_RATE) ?? 1
13
+
14
+ const resolveSampleRate = (envName, fallback) => {
15
+ const parsed = clampRate(process.env[envName])
16
+ return parsed === null ? fallback : parsed
17
+ }
18
+
19
+ const levelSampleRates = {
20
+ default: defaultSampleRate,
21
+ fatal: resolveSampleRate('AZIFY_LOGGER_SAMPLE_RATE_FATAL', defaultSampleRate),
22
+ error: resolveSampleRate('AZIFY_LOGGER_SAMPLE_RATE_ERROR', defaultSampleRate),
23
+ warn: resolveSampleRate('AZIFY_LOGGER_SAMPLE_RATE_WARN', defaultSampleRate),
24
+ info: resolveSampleRate('AZIFY_LOGGER_SAMPLE_RATE_INFO', defaultSampleRate),
25
+ debug: resolveSampleRate(
26
+ 'AZIFY_LOGGER_SAMPLE_RATE_DEBUG',
27
+ defaultSampleRate < 1 ? defaultSampleRate : 0.05
28
+ ),
29
+ trace: resolveSampleRate(
30
+ 'AZIFY_LOGGER_SAMPLE_RATE_TRACE',
31
+ defaultSampleRate < 1 ? defaultSampleRate : 0.02
32
+ )
33
+ }
34
+
35
+ const HTTP_CLIENT_MODE = (process.env.AZIFY_LOGGER_HTTP_CLIENT_LOGGING || 'all').toLowerCase()
36
+
37
+ const httpClientSampleRate = resolveSampleRate(
38
+ 'AZIFY_LOGGER_HTTP_SAMPLE_RATE',
39
+ HTTP_CLIENT_MODE === 'all' ? 1 : 1 // Sempre 1.0 (100%) quando HTTP_CLIENT_MODE === 'all'
40
+ )
41
+
42
+ const sourceSampleRates = {
43
+ 'http-client': httpClientSampleRate,
44
+ logger: resolveSampleRate('AZIFY_LOGGER_SAMPLE_RATE_LOGGER', defaultSampleRate)
45
+ }
46
+
47
+ const shouldSample = (level, source) => {
48
+ const key = typeof level === 'string' ? level.toLowerCase() : 'info'
49
+ let rate = levelSampleRates[key] ?? levelSampleRates.default
50
+ if (source && sourceSampleRates[source] !== undefined) {
51
+ rate = Math.min(rate, sourceSampleRates[source])
52
+ }
53
+ if (rate >= 1) {
54
+ return true
55
+ }
56
+ if (rate <= 0) {
57
+ return false
58
+ }
59
+ return Math.random() < rate
60
+ }
61
+
62
+ const markSource = (meta, source) => {
63
+ if (!meta || typeof meta !== 'object' || !source) {
64
+ return
65
+ }
66
+ Object.defineProperty(meta, '__source', {
67
+ value: source,
68
+ enumerable: false,
69
+ configurable: false
70
+ })
71
+ }
72
+
73
+ module.exports = {
74
+ shouldSample,
75
+ markSource,
76
+ HTTP_CLIENT_MODE
77
+ }
78
+
79
+