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/README.md +295 -0
- package/index.js +114 -0
- package/middleware-express.js +277 -0
- package/middleware-restify.js +280 -0
- package/package.json +47 -0
- package/register-otel.js +39 -0
- package/register-restify.js +203 -0
- package/server.js +229 -0
- package/store.js +42 -0
- package/streams/bunyan.js +60 -0
- package/streams/pino.js +55 -0
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
|
package/streams/pino.js
ADDED
|
@@ -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
|