azify-logger 1.0.2

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/server.js ADDED
@@ -0,0 +1,229 @@
1
+ require('./register-otel.js')
2
+
3
+ const axios = require('axios')
4
+ const express = require('express')
5
+ const cors = require('cors')
6
+ const os = require('os')
7
+ const { trace, context, propagation } = require('@opentelemetry/api')
8
+ const { W3CTraceContextPropagator } = require('@opentelemetry/core')
9
+
10
+ const app = express()
11
+ app.use(express.json())
12
+ app.use(cors())
13
+
14
+ const tracer = trace.getTracer('azify-logger', '1.0.0')
15
+ const propagator = new W3CTraceContextPropagator()
16
+
17
+ const traceContextMap = new Map()
18
+
19
+ async function ensureIndex() {
20
+ const indexName = 'logs-azify'
21
+ const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
22
+ try {
23
+ await axios.head(`${osUrl}/${indexName}`)
24
+ console.log(`✅ Índice ${indexName} já existe no OpenSearch`)
25
+ try {
26
+ await axios.post(`${osUrl}/${indexName}/_mapping`, {
27
+ properties: {
28
+ traceId: { type: 'keyword' },
29
+ spanId: { type: 'keyword' },
30
+ parentSpanId: { type: 'keyword' },
31
+ appName: { type: 'keyword' }
32
+ }
33
+ })
34
+ } catch (mapErr) {}
35
+ } catch (error) {
36
+ if (error.response?.status === 404) {
37
+ try {
38
+ await axios.put(`${osUrl}/${indexName}`, {
39
+ settings: {
40
+ number_of_shards: 1,
41
+ number_of_replicas: 0
42
+ },
43
+ mappings: {
44
+ properties: {
45
+ timestamp: { type: 'date' },
46
+ level: { type: 'keyword' },
47
+ message: { type: 'text' },
48
+ service: {
49
+ properties: {
50
+ name: { type: 'keyword' },
51
+ version: { type: 'keyword' }
52
+ }
53
+ },
54
+ appName: { type: 'keyword' },
55
+ traceId: { type: 'keyword' },
56
+ spanId: { type: 'keyword' },
57
+ parentSpanId: { type: 'keyword' },
58
+ userId: { type: 'keyword' },
59
+ requestId: { type: 'keyword' },
60
+ method: { type: 'keyword' },
61
+ url: { type: 'keyword' },
62
+ statusCode: { type: 'integer' },
63
+ responseTime: { type: 'float' },
64
+ ip: { type: 'ip' },
65
+ userAgent: { type: 'text' },
66
+ environment: { type: 'keyword' },
67
+ hostname: { type: 'keyword' },
68
+ responseBody: { type: 'text' },
69
+ error: {
70
+ properties: {
71
+ message: { type: 'text' },
72
+ stack: { type: 'text' },
73
+ name: { type: 'keyword' }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ })
79
+ console.log(`✅ Índice ${indexName} criado no OpenSearch`)
80
+ } catch (createError) {
81
+ console.error('❌ Erro ao criar índice:', createError.message)
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ ensureIndex()
88
+
89
+ function generateTraceId() {
90
+ return Math.random().toString(36).substring(2, 15) +
91
+ Math.random().toString(36).substring(2, 15)
92
+ }
93
+
94
+ function generateSpanId() {
95
+ return Math.random().toString(36).substring(2, 15)
96
+ }
97
+
98
+ function getOrCreateTraceContext(requestId) {
99
+ if (traceContextMap.has(requestId)) {
100
+ return traceContextMap.get(requestId)
101
+ }
102
+
103
+ const traceContext = {
104
+ traceId: generateTraceId(),
105
+ spanId: generateSpanId(),
106
+ parentSpanId: null
107
+ }
108
+
109
+ traceContextMap.set(requestId, traceContext)
110
+
111
+ setTimeout(() => {
112
+ traceContextMap.delete(requestId)
113
+ }, 30000)
114
+
115
+ return traceContext
116
+ }
117
+
118
+ app.get('/health', (req, res) => {
119
+ res.json({ status: 'ok', service: 'azify-logger' })
120
+ })
121
+
122
+ app.post('/log', async (req, res) => {
123
+ const { level, message, meta } = req.body
124
+ if (!level || !message) {
125
+ return res.status(400).json({ success: false, message: 'Level and message are required.' })
126
+ }
127
+
128
+ const requestId = meta?.requestId
129
+
130
+ let traceContext = null
131
+
132
+ if (meta?.traceId && meta?.spanId) {
133
+ traceContext = {
134
+ traceId: meta.traceId,
135
+ spanId: meta.spanId,
136
+ parentSpanId: meta.parentSpanId || null
137
+ }
138
+ } else {
139
+ try {
140
+ const extractedCtx = propagation.extract(context.active(), req.headers, {
141
+ get (carrier, key) {
142
+ const header = carrier[key]
143
+ if (Array.isArray(header)) return header[0]
144
+ return header
145
+ },
146
+ keys (carrier) {
147
+ return Object.keys(carrier)
148
+ }
149
+ })
150
+
151
+ const spanContext = trace.getSpan(extractedCtx)?.spanContext?.() || null
152
+ if (spanContext && spanContext.traceId && spanContext.spanId) {
153
+ traceContext = {
154
+ traceId: spanContext.traceId,
155
+ spanId: spanContext.spanId,
156
+ parentSpanId: null
157
+ }
158
+ }
159
+ } catch (_) {}
160
+ }
161
+
162
+ if (!traceContext && requestId) {
163
+ traceContext = getOrCreateTraceContext(requestId)
164
+ }
165
+
166
+ if (!traceContext) {
167
+ traceContext = {
168
+ traceId: generateTraceId(),
169
+ spanId: generateSpanId(),
170
+ parentSpanId: null
171
+ }
172
+ }
173
+
174
+ const logEntry = {
175
+ timestamp: meta?.timestamp || new Date().toISOString(),
176
+ level,
177
+ message,
178
+ service: {
179
+ name: meta?.service?.name || 'unknown-service',
180
+ version: meta?.service?.version || '1.0.0'
181
+ },
182
+ appName: meta?.appName || meta?.service?.name || undefined,
183
+ environment: meta?.environment || process.env.NODE_ENV || 'development',
184
+ hostname: meta?.hostname || os.hostname(),
185
+ traceId: traceContext.traceId,
186
+ spanId: traceContext.spanId,
187
+ parentSpanId: traceContext.parentSpanId
188
+ }
189
+
190
+ if (meta) {
191
+ Object.keys(meta).forEach(key => {
192
+ if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'spanId', 'parentSpanId'].includes(key)) {
193
+ logEntry[key] = meta[key]
194
+ }
195
+ })
196
+ }
197
+
198
+ try {
199
+ const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
200
+ await axios.post(`${osUrl}/logs-azify/_doc`, logEntry, {
201
+ headers: { 'Content-Type': 'application/json' }
202
+ })
203
+
204
+ console.log(`✅ [${level.toUpperCase()}] ${message} | traceId: ${traceContext.traceId.substring(0, 8)}... | service: ${logEntry.service.name}`)
205
+ res.json({ success: true, message: 'Log enviado com sucesso' })
206
+ } catch (error) {
207
+ console.error('❌ Erro ao enviar log para OpenSearch:', error.message)
208
+ if (error.response) {
209
+ console.error(' Detalhes:', error.response.data)
210
+ }
211
+ res.status(500).json({ success: false, message: 'Erro ao enviar log para OpenSearch' })
212
+ }
213
+ })
214
+
215
+ const port = process.env.PORT || 3000
216
+
217
+ app.listen(port, () => {
218
+ console.log(`🚀 Azify Logger rodando na porta ${port}`)
219
+ })
220
+
221
+ process.on('SIGTERM', () => {
222
+ console.log('Received SIGTERM, shutting down')
223
+ process.exit(0)
224
+ })
225
+
226
+ process.on('SIGINT', () => {
227
+ console.log('Received SIGINT, shutting down')
228
+ process.exit(0)
229
+ })
package/store.js ADDED
@@ -0,0 +1,42 @@
1
+ const { AsyncLocalStorage } = require('async_hooks')
2
+
3
+ const als = new AsyncLocalStorage()
4
+
5
+ function generateId(bytes = 16) {
6
+ return require('crypto').randomBytes(bytes).toString('hex')
7
+ }
8
+
9
+ function toTraceId(hex32) {
10
+ // ensure 32 hex chars
11
+ const h = (hex32 || '').padStart(32, '0').slice(0, 32)
12
+ return `${h.substring(0, 8)}-${h.substring(8, 12)}-${h.substring(12, 16)}-${h.substring(16, 20)}-${h.substring(20, 32)}`
13
+ }
14
+
15
+ function startRequestContext(initial = {}) {
16
+ const traceHex = initial.traceHex || generateId(16) // 16 bytes -> 32 hex
17
+ const spanHex = initial.spanHex || generateId(8) // 8 bytes -> 16 hex
18
+ const ctx = {
19
+ traceId: toTraceId(traceHex),
20
+ spanId: spanHex,
21
+ parentSpanId: initial.parentSpanId || null,
22
+ requestId: initial.requestId
23
+ }
24
+ return ctx
25
+ }
26
+
27
+ function runWithRequestContext(ctx, fn) {
28
+ return als.run(ctx, fn)
29
+ }
30
+
31
+ function getRequestContext() {
32
+ return als.getStore() || null
33
+ }
34
+
35
+ module.exports = {
36
+ als,
37
+ startRequestContext,
38
+ runWithRequestContext,
39
+ getRequestContext
40
+ }
41
+
42
+
@@ -0,0 +1,60 @@
1
+ const { AsyncLocalStorage } = require('async_hooks')
2
+ const axios = require('axios')
3
+
4
+ const globalContext = new Map()
5
+
6
+ function createBunyanStream(options = {}) {
7
+ const loggerUrl = options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3000'
8
+ const serviceName = options.serviceName || process.env.APP_NAME || 'app'
9
+ const environment = options.environment || process.env.NODE_ENV || 'development'
10
+
11
+ return {
12
+ write(record) {
13
+ const level = (record.level >= 50) ? 'error'
14
+ : (record.level >= 40) ? 'warn'
15
+ : (record.level >= 30) ? 'info'
16
+ : (record.level >= 20) ? 'debug' : 'trace'
17
+
18
+ let traceId, spanId, requestId
19
+
20
+ try {
21
+ const { getRequestContext } = require('../store')
22
+ const context = getRequestContext()
23
+ if (context) {
24
+ traceId = context.traceId
25
+ spanId = context.spanId
26
+ requestId = context.requestId
27
+ }
28
+ } catch (_) {}
29
+
30
+ if (!traceId || !spanId) {
31
+ requestId = record.requestId || record.req_id || record.reqId
32
+ if (requestId) {
33
+ const crypto = require('crypto')
34
+ const hash = crypto.createHash('sha256').update(String(requestId)).digest('hex')
35
+ traceId = `${hash.substring(0, 8)}-${hash.substring(8, 12)}-${hash.substring(12, 16)}-${hash.substring(16, 20)}-${hash.substring(20, 32)}`
36
+ spanId = hash.substring(0, 16)
37
+ }
38
+ }
39
+
40
+ const headers = {}
41
+
42
+ const meta = {
43
+ ...record,
44
+ service: { name: serviceName, version: record.service?.version || '1.0.0' },
45
+ environment,
46
+ timestamp: new Date().toISOString(),
47
+ hostname: require('os').hostname(),
48
+ requestId: requestId || record.requestId || record.req_id || record.reqId,
49
+ ...(traceId && { traceId }),
50
+ ...(spanId && { spanId })
51
+ }
52
+
53
+ const payload = { level, message: record.msg || record.message || 'log', meta }
54
+
55
+ axios.post(`${loggerUrl}/log`, payload, { headers, timeout: 3000 }).catch(() => {})
56
+ }
57
+ }
58
+ }
59
+
60
+ module.exports = createBunyanStream
@@ -0,0 +1,55 @@
1
+ const { context, propagation, trace } = require('@opentelemetry/api')
2
+ const { W3CTraceContextPropagator } = require('@opentelemetry/core')
3
+ const axios = require('axios')
4
+
5
+ function createPinoStream(options = {}) {
6
+ const loggerUrl = options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3000'
7
+ const serviceName = options.serviceName || process.env.APP_NAME || 'app'
8
+ const environment = options.environment || process.env.NODE_ENV || 'development'
9
+
10
+ return {
11
+ write(chunk) {
12
+ let record
13
+ try { record = typeof chunk === 'string' ? JSON.parse(chunk) : chunk } catch (_) { return }
14
+ const levelMap = { 60: 'fatal', 50: 'error', 40: 'warn', 30: 'info', 20: 'debug', 10: 'trace' }
15
+ const level = levelMap[record.level] || 'info'
16
+
17
+ let traceId, spanId
18
+ try {
19
+ const span = trace.getSpan(context.active())
20
+ const spanContext = span?.spanContext()
21
+ if (spanContext) {
22
+ traceId = spanContext.traceId
23
+ spanId = spanContext.spanId
24
+ }
25
+ } catch (_) {}
26
+
27
+ const headers = {}
28
+ try {
29
+ const propagator = new W3CTraceContextPropagator()
30
+ propagation.setGlobalPropagator(propagator)
31
+ propagation.inject(context.active(), headers, {
32
+ set (carrier, key, value) {
33
+ carrier[key] = value
34
+ }
35
+ })
36
+ } catch (_) {}
37
+
38
+ const meta = {
39
+ ...record,
40
+ service: { name: serviceName, version: record.service?.version || '1.0.0' },
41
+ environment,
42
+ timestamp: new Date().toISOString(),
43
+ hostname: require('os').hostname(),
44
+ requestId: record.requestId || record.req_id || record.reqId,
45
+ ...(traceId && { traceId }),
46
+ ...(spanId && { spanId })
47
+ }
48
+
49
+ const payload = { level, message: record.msg || record.message || 'log', meta }
50
+ axios.post(`${loggerUrl}/log`, payload, { headers, timeout: 3000 }).catch(() => {})
51
+ }
52
+ }
53
+ }
54
+
55
+ module.exports = createPinoStream