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 ADDED
@@ -0,0 +1,295 @@
1
+ # 🚀 Azify Logger - Logging Centralizado
2
+
3
+ Sistema de logging centralizado com OpenTelemetry e OpenSearch para múltiplas aplicações.
4
+
5
+ ## 📦 Instalação
6
+
7
+ Na sua aplicação, adicione ao `package.json`:
8
+
9
+ ```json
10
+ {
11
+ "dependencies": {
12
+ "azify-logger": "latest"
13
+ }
14
+ }
15
+ ```
16
+
17
+ Ou via npm:
18
+
19
+ ```bash
20
+ npm install azify-logger
21
+ ```
22
+
23
+ ## ⚡ Utilização
24
+
25
+ ### Para aplicações Restify:
26
+
27
+ **1. No arquivo de inicialização:**
28
+ ```javascript
29
+ #!/usr/bin/env node
30
+ require('azify-logger/register-otel');
31
+ // Resto do código...
32
+ ```
33
+
34
+ **2. No servidor Restify:**
35
+ ```javascript
36
+ import { middleware as azifyMiddleware } from 'azify-logger'
37
+
38
+ const server = restify.createServer()
39
+ server.use(azifyMiddleware.restify())
40
+ ```
41
+
42
+ **3. Variável de ambiente:**
43
+ ```bash
44
+ APP_NAME=nome-app
45
+ ```
46
+
47
+ **PRONTO! 🎉**
48
+
49
+ ### Para aplicações Express:
50
+
51
+ ```javascript
52
+ require('azify-logger')
53
+
54
+ const express = require('express')
55
+ const app = express()
56
+ // Logs automáticos via OpenTelemetry
57
+ ```
58
+
59
+ ### ❌ O que NÃO precisa:
60
+ - ❌ Instalar dependências OpenTelemetry separadamente
61
+ - ❌ Configurar tracing manualmente
62
+
63
+ ## ⚙️ Variáveis de Ambiente
64
+
65
+ | Variável | Padrão | Descrição |
66
+ |----------|-------|-----------|
67
+ | `APP_NAME` | - | Nome da aplicação |
68
+ | `AZIFY_LOGGER_URL` | `http://localhost:3000` | URL do logger |
69
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318/v1/traces` | OTLP endpoint |
70
+ | `NODE_ENV` | `development` | Ambiente |
71
+ | `AZIFY_LOGGER_AUTOREG_DISABLE` | `""` | Se `"1"`, desativa auto-registro do OTEL |
72
+
73
+ **Para desenvolvimento local:** só precisa de `APP_NAME` (o resto usa defaults)
74
+
75
+ **Para produção:** configure todas as URLs apontando para servidores de produção
76
+
77
+ ### Docker e Node antigos
78
+
79
+ Se seu container usa uma versão antiga do Node e você ver erros de inicialização do OpenTelemetry (ex.: `Cannot find module 'node:events'` saindo de `google-logging-utils` ou `@opentelemetry/resource-detector-gcp`), defina no container:
80
+
81
+ ```bash
82
+ AZIFY_LOGGER_AUTOREG_DISABLE=1
83
+ ```
84
+
85
+ Isso evita carregar `register-otel.js` e mantém os envios de log via streams/middleware normalmente, permitindo visualização no OpenSearch.
86
+
87
+ ## 🎯 O Que Você Ganha
88
+
89
+ - ✅ **Zero Config**: OpenTelemetry habilitado automaticamente
90
+ - ✅ **Logs Completos**: Headers, body, query params, status
91
+ - ✅ **Trace Consistente**: REQUEST e RESPONSE com mesmo traceId/spanId
92
+ - ✅ **Genérico**: Funciona com Bunyan, Pino, console.log ou qualquer logger
93
+ - ✅ **Centralizado**: Todos os logs no OpenSearch
94
+
95
+ ## 🔧 Uso Avançado
96
+
97
+ ### Com Bunyan existente (adicionar stream)
98
+
99
+ Se sua app **já tem Bunyan** e você quer adicionar o azify-logger como stream adicional:
100
+
101
+ ```javascript
102
+ // Forma segura com try/catch (não quebra se não tiver instalado)
103
+ let createAzifyBunyanStream
104
+ try {
105
+ createAzifyBunyanStream = require('azify-logger/streams/bunyan')
106
+ } catch (_) {
107
+ createAzifyBunyanStream = null
108
+ }
109
+
110
+ const bunyan = require('bunyan')
111
+ const streams = [
112
+ { level: 'info', stream: process.stdout }
113
+ ]
114
+
115
+ // Adiciona stream do azify-logger se disponível
116
+ if (createAzifyBunyanStream) {
117
+ streams.push({
118
+ level: 'info',
119
+ type: 'raw',
120
+ stream: createAzifyBunyanStream({
121
+ loggerUrl: process.env.AZIFY_LOGGER_URL || 'http://localhost:3000',
122
+ serviceName: process.env.APP_NAME || 'app'
123
+ })
124
+ })
125
+ }
126
+
127
+ const logger = bunyan.createLogger({ name: 'app', streams })
128
+ logger.info('Teste') // Vai para stdout E para azify-logger
129
+ ```
130
+
131
+ ### Com Bunyan (auto-patch - mais simples)
132
+
133
+ ```javascript
134
+ require('azify-logger')
135
+
136
+ const bunyan = require('bunyan')
137
+ const log = bunyan.createLogger({ name: 'nome-app' })
138
+
139
+ log.info('Teste') // Automaticamente enviado para azify-logger
140
+ ```
141
+
142
+ ### Com Pino
143
+
144
+ ```javascript
145
+ const { streams } = require('azify-logger')
146
+ const pino = require('pino')
147
+
148
+ const logger = pino({ level: 'info' }, streams.createPinoStream({
149
+ loggerUrl: 'http://localhost:3000',
150
+ serviceName: 'nome-app'
151
+ }))
152
+
153
+ logger.info('Teste')
154
+ ```
155
+
156
+ ### Logger Direto (sem Bunyan/Pino)
157
+
158
+ ```javascript
159
+ const { createAzifyLogger } = require('azify-logger')
160
+
161
+ const logger = createAzifyLogger({
162
+ serviceName: 'nome-app',
163
+ loggerUrl: 'http://localhost:3000'
164
+ })
165
+
166
+ logger.info('Mensagem', { userId: '123' })
167
+ logger.error('Erro', new Error('Falha'), { context: 'payment' })
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 🛠️ Setup do Serviço de Logging (Infra)
173
+
174
+ Se você precisa subir a infraestrutura do azify-logger:
175
+
176
+ ### 1. Iniciar serviços
177
+
178
+ ```bash
179
+ ./start-docker.sh
180
+ ```
181
+
182
+ Aguarde alguns minutos. Você verá:
183
+
184
+ ```
185
+ ✅ Tudo pronto!
186
+ 📊 OpenSearch: http://localhost:9200
187
+ 🎨 OpenSearch Dashboards: http://localhost:5601
188
+ 📝 Logger API: http://localhost:3000
189
+ 🔭 OTEL Collector: http://localhost:4318
190
+ ```
191
+
192
+ ### 2. Testar
193
+
194
+ ```bash
195
+ curl -X POST http://localhost:3000/log \
196
+ -H "Content-Type: application/json" \
197
+ -d '{
198
+ "level": "info",
199
+ "message": "Teste",
200
+ "meta": {"service": {"name": "teste"}}
201
+ }'
202
+ ```
203
+
204
+ ### 3. Ver logs
205
+
206
+ **OpenSearch Dashboards:**
207
+ ```bash
208
+ http://localhost:5601
209
+ ```
210
+
211
+ **Como usar:**
212
+ Acesse: http://localhost:5601
213
+
214
+ ## 📊 Estrutura dos Logs
215
+
216
+ Cada log no OpenSearch contém:
217
+
218
+ ```json
219
+ {
220
+ "timestamp": "2025-10-01T10:15:30.123Z",
221
+ "level": "info",
222
+ "message": "[REQUEST] POST /api/payment",
223
+ "service": {
224
+ "name": "nome-app",
225
+ "version": "1.0.0"
226
+ },
227
+ "traceId": "abc123...",
228
+ "spanId": "xyz789...",
229
+ "method": "POST",
230
+ "url": "/api/payment",
231
+ "requestBody": { ... },
232
+ "statusCode": 200,
233
+ "responseTime": 123.45,
234
+ "responseBody": { ... },
235
+ "environment": "production",
236
+ "hostname": "app-server-01"
237
+ }
238
+ ```
239
+
240
+ ## 🔍 Como Funciona
241
+
242
+ 1. **OpenTelemetry** cria um span para cada request HTTP
243
+ 2. **Middleware** captura request e response com o **mesmo span ativo**
244
+ 3. **Logger** envia ambos os logs com **traceId e spanId iguais**
245
+ 4. **OpenSearch** indexa e permite buscar por traceId
246
+
247
+ ## 📖 Arquitetura
248
+
249
+ ```
250
+ ┌─────────────┐
251
+ │ aplicação │
252
+ └──────┬──────┘
253
+ │ HTTP POST
254
+
255
+ ┌─────────────┐
256
+ │ azify-logger│ (recebe logs)
257
+ └──────┬──────┘
258
+
259
+
260
+ ┌─────────────┐
261
+ │ OpenSearch │ (armazena)
262
+ └──────┬──────┘
263
+
264
+
265
+ ┌─────────────┐
266
+ │ Dashboards |
267
+ | OpenSearch │ (visualiza)
268
+ └─────────────┘
269
+ ```
270
+
271
+ ## 📈 OpenSearch Dashboards
272
+
273
+ O azify-logger usa OpenSearch Dashboards para visualização dos logs:
274
+
275
+ ### 🎯 Funcionalidades Disponíveis
276
+
277
+ 1. **📊 Discover** - Visualização e busca de logs
278
+ - Filtros por serviço, app, traceId, statusCode
279
+ - Busca em tempo real
280
+ - Visualização detalhada de cada log
281
+
282
+ 2. **📈 Dashboard** - "Application Health Dashboard"
283
+ - **Taxa de Sucesso** - Percentual de requisições 2xx
284
+ - **Latência Média** - Tempo médio de resposta
285
+ - **Erros 4xx/5xx** - Contador de erros
286
+ - **Requisições por Minuto** - Gráfico temporal
287
+
288
+ ### 🚀 Acesso
289
+
290
+ ```bash
291
+ # Local
292
+ http://localhost:5601
293
+
294
+ # Sem autenticação necessária (desenvolvimento)
295
+ ```
package/index.js ADDED
@@ -0,0 +1,114 @@
1
+ const axios = require('axios')
2
+ const { context, propagation, trace } = require('@opentelemetry/api')
3
+ const { W3CTraceContextPropagator } = require('@opentelemetry/core')
4
+
5
+ if (process.env.AZIFY_LOGGER_AUTOREG_DISABLE !== '1') {
6
+ try { require('./register-otel') } catch (_) {}
7
+ try { require('./register') } catch (_) {}
8
+ try { require('./register-restify') } catch (_) {}
9
+ }
10
+
11
+ class AzifyLogger {
12
+ constructor(options = {}) {
13
+ this.options = {
14
+ serviceName: options.serviceName || 'azipay',
15
+ loggerUrl: options.loggerUrl || 'http://localhost:3000',
16
+ environment: options.environment || process.env.NODE_ENV || 'development',
17
+ ...options
18
+ }
19
+
20
+ this.propagator = new W3CTraceContextPropagator()
21
+ propagation.setGlobalPropagator(this.propagator)
22
+ }
23
+
24
+ async log(level, message, meta = {}) {
25
+ const span = trace.getSpan(context.active())
26
+ const spanContext = span?.spanContext()
27
+
28
+ const logData = {
29
+ level,
30
+ message,
31
+ meta: {
32
+ ...meta,
33
+ service: {
34
+ name: this.options.serviceName,
35
+ version: meta.service?.version || '1.0.0'
36
+ },
37
+ environment: this.options.environment,
38
+ timestamp: new Date().toISOString(),
39
+ hostname: require('os').hostname()
40
+ }
41
+ }
42
+
43
+ if (spanContext) {
44
+ logData.meta.traceId = spanContext.traceId
45
+ logData.meta.spanId = spanContext.spanId
46
+ }
47
+
48
+ try {
49
+ const headers = {}
50
+ try {
51
+ propagation.inject(context.active(), headers, {
52
+ set (carrier, key, value) {
53
+ carrier[key] = value
54
+ }
55
+ })
56
+ } catch (_) {}
57
+
58
+ await axios.post(`${this.options.loggerUrl}/log`, logData, {
59
+ timeout: 5000,
60
+ headers
61
+ })
62
+ } catch (error) {
63
+ console.error('Erro ao enviar log:', error.message)
64
+ }
65
+ }
66
+
67
+ info(message, meta = {}) {
68
+ return this.log('info', message, meta)
69
+ }
70
+
71
+ error(message, error = null, meta = {}) {
72
+ const logMeta = { ...meta }
73
+ if (error) {
74
+ logMeta.error = {
75
+ message: error.message,
76
+ stack: error.stack,
77
+ name: error.name
78
+ }
79
+ }
80
+ return this.log('error', message, logMeta)
81
+ }
82
+
83
+ warn(message, meta = {}) {
84
+ return this.log('warn', message, meta)
85
+ }
86
+
87
+ debug(message, meta = {}) {
88
+ return this.log('debug', message, meta)
89
+ }
90
+ }
91
+
92
+ function createAzifyLogger(options = {}) {
93
+ return new AzifyLogger(options)
94
+ }
95
+
96
+ function createAzifyLoggerFromEnv() {
97
+ return createAzifyLogger({
98
+ serviceName: process.env.APP_NAME || 'azipay',
99
+ loggerUrl: process.env.AZIFY_LOGGER_URL || 'http://localhost:3000',
100
+ environment: process.env.NODE_ENV || 'development'
101
+ })
102
+ }
103
+
104
+ module.exports = AzifyLogger
105
+ module.exports.createAzifyLogger = createAzifyLogger
106
+ module.exports.createAzifyLoggerFromEnv = createAzifyLoggerFromEnv
107
+ module.exports.streams = {
108
+ createBunyanStream: require('./streams/bunyan'),
109
+ createPinoStream: require('./streams/pino')
110
+ }
111
+ module.exports.middleware = {
112
+ restify: require('./middleware-restify'),
113
+ express: require('./middleware-express')
114
+ }
@@ -0,0 +1,277 @@
1
+ const axios = require('axios')
2
+ const { als, runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
3
+
4
+ function createExpressLoggingMiddleware(options = {}) {
5
+ const config = {
6
+ serviceName: options.serviceName || process.env.APP_NAME || 'assemble',
7
+ loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3000',
8
+ environment: options.environment || process.env.NODE_ENV || 'development'
9
+ }
10
+
11
+ async function sendLog(level, message, meta = {}) {
12
+ const logData = {
13
+ level,
14
+ message,
15
+ meta: {
16
+ ...meta,
17
+ service: {
18
+ name: config.serviceName,
19
+ version: '1.0.0'
20
+ },
21
+ environment: config.environment,
22
+ timestamp: new Date().toISOString(),
23
+ hostname: require('os').hostname()
24
+ }
25
+ }
26
+
27
+ try {
28
+ await axios.post(`${config.loggerUrl}/log`, logData, {
29
+ timeout: 5000
30
+ })
31
+ } catch (error) {}
32
+ }
33
+
34
+ return function azifyExpressLoggingMiddleware(req, res, next) {
35
+ const startTime = Date.now()
36
+ const requestId = req.requestId || require('uuid').v4()
37
+ const middlewareId = require('uuid').v4().substring(0, 8)
38
+
39
+ const reqCtx = startRequestContext({ requestId })
40
+ req.__azifyTraceId = reqCtx.traceId
41
+ req.__azifySpanId = reqCtx.spanId
42
+ req.__azifyParentSpanId = reqCtx.parentSpanId
43
+
44
+ const requestTraceId = req.__azifyTraceId
45
+ const requestSpanId = req.__azifySpanId
46
+ const requestParentSpanId = req.__azifyParentSpanId
47
+
48
+ let baseUrl = req.url
49
+ if (baseUrl.includes('?')) {
50
+ baseUrl = baseUrl.substring(0, baseUrl.indexOf('?'))
51
+ }
52
+ baseUrl = baseUrl.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/{id}')
53
+ baseUrl = baseUrl.replace(/\/[0-9]+/g, '/{id}')
54
+
55
+ req._azifyRequestData = {
56
+ requestId,
57
+ method: req.method,
58
+ url: req.url,
59
+ baseUrl: baseUrl,
60
+ path: req.url,
61
+ headers: req.headers,
62
+ query: req.query,
63
+ userAgent: req.headers['user-agent'],
64
+ ip: req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip,
65
+ traceId: requestTraceId,
66
+ spanId: requestSpanId,
67
+ parentSpanId: requestParentSpanId
68
+ }
69
+
70
+ if (req.method === 'GET') {
71
+ sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
72
+ } else {
73
+ if (req.body !== undefined) {
74
+ req._azifyRequestData.requestBody = req.body
75
+ }
76
+ sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
77
+ }
78
+
79
+ res.on('finish', () => {
80
+ if (!responseLogged) {
81
+ logResponse()
82
+ responseLogged = true
83
+ }
84
+ })
85
+
86
+ res.on('close', () => {
87
+ if (!responseLogged) {
88
+ logResponse()
89
+ responseLogged = true
90
+ }
91
+ })
92
+
93
+ let sentBody
94
+ let responseLogged = false
95
+
96
+ const originalSend = res.send && res.send.bind(res)
97
+ if (originalSend) {
98
+ res.send = function patchedSend() {
99
+ try {
100
+ if (arguments.length === 1) {
101
+ sentBody = arguments[0]
102
+ } else if (arguments.length >= 2) {
103
+ sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
104
+ }
105
+ } catch (_) {}
106
+
107
+ if (!responseLogged) {
108
+ logResponse()
109
+ responseLogged = true
110
+ }
111
+
112
+ return originalSend.apply(this, arguments)
113
+ }
114
+ }
115
+
116
+ const originalStatus = res.status
117
+ res.status = function(code) {
118
+ res._actualStatusCode = code
119
+ return originalStatus.call(this, code)
120
+ }
121
+
122
+ const originalWriteHead = res.writeHead
123
+ res.writeHead = function(statusCode, statusMessage, headers) {
124
+ res._actualStatusCode = statusCode
125
+ if (typeof statusMessage === 'object') {
126
+ headers = statusMessage
127
+ statusMessage = undefined
128
+ }
129
+ return originalWriteHead.call(this, statusCode, statusMessage, headers)
130
+ }
131
+
132
+ const originalJsonMethod = res.json
133
+ res.json = function(code, body) {
134
+ try {
135
+ if (arguments.length === 1) {
136
+ sentBody = arguments[0]
137
+ } else if (arguments.length >= 2) {
138
+ sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
139
+ }
140
+ } catch (_) {}
141
+
142
+ if (typeof code === 'number') {
143
+ res._actualStatusCode = code
144
+ } else {
145
+ const errorObj = arguments.length === 1 ? arguments[0] : (typeof arguments[0] === 'number' ? arguments[1] : arguments[0])
146
+
147
+ if (errorObj && errorObj.constructor && errorObj.constructor.name === 'ErrCtor') {
148
+ const errorName = errorObj.toString()
149
+ if (errorName.includes('InternalServerError') || errorName.includes('InternalError')) {
150
+ res._actualStatusCode = 500
151
+ } else if (errorName.includes('BadRequest') || errorName.includes('BadDigest')) {
152
+ res._actualStatusCode = 400
153
+ } else if (errorName.includes('NotFound')) {
154
+ res._actualStatusCode = 404
155
+ } else if (errorName.includes('Unauthorized')) {
156
+ res._actualStatusCode = 401
157
+ } else if (errorName.includes('Forbidden')) {
158
+ res._actualStatusCode = 403
159
+ } else {
160
+ res._actualStatusCode = 500
161
+ }
162
+ } else {
163
+ res._actualStatusCode = res.statusCode || 200
164
+ }
165
+ }
166
+
167
+ if (!responseLogged) {
168
+ logResponse()
169
+ responseLogged = true
170
+ }
171
+
172
+ return originalJsonMethod.apply(this, arguments)
173
+ }
174
+
175
+ res.on('finish', function() {
176
+ if (!responseLogged) {
177
+ logResponse()
178
+ responseLogged = true
179
+ }
180
+ })
181
+
182
+ const originalEnd = res.end
183
+ res.end = function(chunk, encoding) {
184
+ const duration = Date.now() - startTime
185
+
186
+ let responseBody = sentBody
187
+ try {
188
+ if (responseBody == null && chunk) {
189
+ if (Buffer.isBuffer(chunk)) {
190
+ responseBody = chunk.toString('utf8')
191
+ } else if (typeof chunk === 'string') {
192
+ responseBody = chunk
193
+ } else {
194
+ responseBody = JSON.stringify(chunk)
195
+ }
196
+ }
197
+ } catch (_) {}
198
+
199
+ if (!responseLogged) {
200
+ logResponse()
201
+ responseLogged = true
202
+ }
203
+
204
+ originalEnd.call(this, chunk, encoding)
205
+ }
206
+
207
+ function logResponse() {
208
+ const duration = Date.now() - startTime
209
+
210
+ let responseBody = sentBody
211
+
212
+ let serializedResponseBody
213
+ try {
214
+ if (typeof responseBody === 'string') {
215
+ serializedResponseBody = responseBody
216
+ } else if (Array.isArray(responseBody)) {
217
+ serializedResponseBody = JSON.stringify(responseBody)
218
+ } else if (responseBody && typeof responseBody === 'object') {
219
+ if (responseBody.toJSON && typeof responseBody.toJSON === 'function') {
220
+ serializedResponseBody = JSON.stringify(responseBody.toJSON())
221
+ } else if (responseBody.toString && typeof responseBody.toString === 'function' && responseBody.toString() !== '[object Object]') {
222
+ serializedResponseBody = responseBody.toString()
223
+ } else {
224
+ serializedResponseBody = JSON.stringify(responseBody, (key, value) => {
225
+ if (typeof value === 'function') {
226
+ return '[Function]'
227
+ }
228
+ if (value instanceof Error) {
229
+ return { name: value.name, message: value.message, stack: value.stack }
230
+ }
231
+ return value
232
+ }, null, 0)
233
+ }
234
+ } else {
235
+ serializedResponseBody = responseBody != null ? String(responseBody) : ''
236
+ }
237
+ } catch (error) {
238
+ try {
239
+ serializedResponseBody = JSON.stringify(responseBody, null, 2)
240
+ } catch (secondError) {
241
+ serializedResponseBody = '[Complex object - serialization failed]'
242
+ }
243
+ }
244
+
245
+ const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
246
+
247
+ const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
248
+ ? `[RESPONSE] ${serializedResponseBody}`
249
+ : `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
250
+
251
+ const responseData = {
252
+ ...req._azifyRequestData,
253
+ requestBody: req.body,
254
+ statusCode: statusCode,
255
+ responseTime: duration,
256
+ responseHeaders: res.getHeaders ? res.getHeaders() : {},
257
+ responseBody: serializedResponseBody
258
+ }
259
+
260
+ try { res._azifyResponseLogged = true } catch (_) {}
261
+ sendLog('info', responseMessage, responseData)
262
+ }
263
+
264
+ req._azifyContext = reqCtx
265
+
266
+ try {
267
+ runWithRequestContext(reqCtx, () => {
268
+ next()
269
+ })
270
+ } catch (error) {
271
+ console.error('Error in azify middleware:', error)
272
+ next()
273
+ }
274
+ }
275
+ }
276
+
277
+ module.exports = createExpressLoggingMiddleware