aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/org/onap/aai/babel/logging/LogHelper.java
blob: c118d8aa69b25cb778b13239a1c719dd83bb6959 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
/**
 * ============LICENSE_START=======================================================
 * org.onap.aai
 * ================================================================================
 * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
 * Copyright © 2017-2018 European Software Marketing Ltd.
 * ================================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ============LICENSE_END=========================================================
 */
package org.onap.aai.babel.logging;

import static com.att.eelf.configuration.Configuration.MDC_SERVICE_NAME;

import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.core.FileAppender;
import com.att.eelf.configuration.EELFLogger;
import com.att.eelf.configuration.EELFManager;
import com.att.eelf.i18n.EELFResolvableErrorEnum;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.servlet.ServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.time.StopWatch;
import org.onap.aai.babel.request.RequestHeaders;
import org.onap.aai.cl.api.LogFields;
import org.onap.aai.cl.api.Logger;
import org.onap.aai.cl.mdc.MdcContext;
import org.onap.aai.cl.mdc.MdcOverride;
import org.onap.aai.restclient.client.Headers;
import org.slf4j.MDC;

/*-
 * This Log Helper mimics the interface of a Common Logging Logger
 * but adds helper methods for audit and metrics logging requirements.
 *
 * Messages are logged to the appropriate EELF functional logger as described below.
 *
 * Error Log: INFO/WARN/ERROR/FATAL
 * Debug Log: DEBUG/TRACE
 * Audit Log: summary view of transaction processing
 * Metrics Log: detailed timings of transaction processing interactions
 *
 * Audit and Metrics log messages record the following fields:
 *
 * RequestID   - an RFC4122 UUID for the transaction request
 * ServiceName - the API provided by this service
 * PartnerName - invoker of the API
 * ClassName   - name of the class creating the log record
 *
 * The above list is not exhaustive.
 */
public enum LogHelper implements Logger {
    INSTANCE; // Joshua Bloch's singleton pattern

    @FunctionalInterface
    public interface TriConsumer<T, U, V> {
        public void accept(T t, U u, V v);
    }

    /** Audit log message status code values. See {@code MdcParameter.STATUS_CODE} */
    public enum StatusCode {
        COMPLETE,
        ERROR;
    }

    /**
     * Mapped Diagnostic Context parameter names.
     *
     * <p>
     * Note that MdcContext.MDC_START_TIME is used for audit messages, and indicates the start of a transaction.
     * Messages in the metrics log record sub-operations of a transaction and thus use different timestamps.
     */
    public enum MdcParameter {
        REQUEST_ID(MdcContext.MDC_REQUEST_ID),
        CLASS_NAME("ClassName"),
        BEGIN_TIMESTAMP("BeginTimestamp"),
        END_TIMESTAMP("EndTimestamp"),
        ELAPSED_TIME("ElapsedTime"),
        STATUS_CODE("StatusCode"),
        RESPONSE_CODE("ResponseCode"),
        RESPONSE_DESCRIPTION("ResponseDescription"),
        TARGET_ENTITY("TargetEntity"),
        TARGET_SERVICE_NAME("TargetServiceName"),
        USER("User");

        private final String parameterName;

        MdcParameter(String parameterName) {
            this.parameterName = parameterName;
        }

        /**
         * Get the MDC logging context parameter name as referenced by the logback configuration
         *
         * @return the MDC parameter name
         */
        public String value() {
            return parameterName;
        }
    }

    /** Our externally advertised service API */
    private static final String SERVICE_NAME_VALUE = "AAI-BAS";

    private static final EELFLogger errorLogger = EELFManager.getInstance().getErrorLogger();
    private static final EELFLogger debugLogger = EELFManager.getInstance().getDebugLogger();
    private static final EELFLogger auditLogger = EELFManager.getInstance().getAuditLogger();
    private static final EELFLogger metricsLogger = EELFManager.getInstance().getMetricsLogger();

    /** Formatting for timestamps logged as Strings (from the MDC) */
    private DateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");

    // Records the elapsed time (since the start of servicing a request) for audit logging
    private StopWatch auditStopwatch;

    /**
     * Initialises the MDC (logging context) with default values, to support any logging of messages BEFORE the
     * startAudit() method is invoked.
     */
    private LogHelper() {
        setContextValue(MDC_SERVICE_NAME, SERVICE_NAME_VALUE);
        // This value is not expected to be used in the default logging configuration
        setContextValue(MdcContext.MDC_START_TIME, timestampFormat.format(new Date()));
    }

    /**
     * Begin recording transaction information for a new request. This data is intended for logging purposes. This
     * method does not actually write any messages to the log(s).
     *
     * <p>
     * Initialise the MDC logging context for auditing and metrics, using the HTTP request headers. This information
     * includes: the correlation ID, local application service name/ID, calling host details and authentication data.
     *
     * <p>
     * The request object is used to find the client details (e.g. IP address)
     *
     * @param headers raw HTTP headers
     * @param servletRequest the request
     */
    public void startAudit(final HttpHeaders headers, ServletRequest servletRequest) {
        auditStopwatch = new StopWatch();
        auditStopwatch.start();

        Optional<String> requestId = Optional.empty();
        String serviceInstanceId = null;
        Optional<String> partnerName = Optional.empty();

        if (headers != null) {
            RequestHeaders requestHeaders = new RequestHeaders(headers);
            requestId = Optional.ofNullable(requestHeaders.getCorrelationId());
            serviceInstanceId = requestHeaders.getInstanceId();
            partnerName = Optional.ofNullable(headers.getHeaderString(Headers.FROM_APP_ID));
        }

        String clientHost = null;
        String clientIPAddress = null;
        String user = "<UNKNOWN_USER>";

        if (servletRequest != null) {
            clientHost = servletRequest.getRemoteHost();
            clientIPAddress = servletRequest.getRemoteAddr();

            if (!partnerName.isPresent()) {
                partnerName = Optional.ofNullable(clientHost);
            }
        }

        // Populate standard MDC keys - note that the initialize method calls MDC.clear()
        MdcContext.initialize(requestId.orElse("missing-request-id"), SERVICE_NAME_VALUE, serviceInstanceId,
                partnerName.orElse("<UNKNOWN_PARTNER>"), clientIPAddress);

        setContextValue(MdcParameter.USER, user);
        setContextValue(MdcContext.MDC_REMOTE_HOST, clientHost);
    }

    /**
     * Store a value in the current thread's logging context.
     *
     * @param key non-null parameter name
     * @param value the value to store against the key
     */
    public void setContextValue(String key, String value) {
        debug(key + "=" + value);
        MDC.put(key, value);
    }

    /**
     * Store a value in the current thread's logging context.
     *
     * @param param identifier of the context parameter
     * @param value the value to store for this parameter
     */
    public void setContextValue(MdcParameter param, String value) {
        setContextValue(param.value(), value);
    }

    /**
     * Set a value in the current thread's logging context, only if this is not already set.
     *
     * @param key non-null parameter name
     * @param value the value to store against the key (only if the current value is null)
     */
    public void setDefaultContextValue(String key, String value) {
        if (MDC.get(key) == null) {
            setContextValue(key, value);
        }
    }

    /**
     * Set a value in the current thread's logging context, only if this is not already set.
     *
     * @param param identifier of the context parameter
     * @param value the value to store for this parameter (only if the current value is null)
     */
    public void setDefaultContextValue(MdcParameter param, String value) {
        setContextValue(param.value(), value);
    }

    /** Clear all logging context values. This should be called at start-up only. */
    public void clearContext() {
        debug("Clearing MDC context");
        MDC.clear();
    }

    /**
     * Log an audit message to the audit logger. This method is expected to be called when a response is returned to the
     * caller and/or when the processing of the request completes.
     *
     * @param status status of the service request: COMPLETE/ERROR
     * @param responseCode optional application error code
     * @param responseDescription human-readable description of the response code
     * @param args the argument(s) required to populate the Audit Message log content
     */
    public void logAudit(StatusCode status, String responseCode, String responseDescription, final String... args) {
        if (auditStopwatch == null) {
            debug("Unexpected program state: audit stopwatch not started");
            auditStopwatch = new StopWatch();
            auditStopwatch.start();
        }

        if (auditLogger.isInfoEnabled()) {
            setMdcElapsedTime(auditStopwatch);
            setContextValue(MdcParameter.STATUS_CODE, status.toString());
            setContextValue(MdcParameter.RESPONSE_CODE, responseCode);
            setContextValue(MdcParameter.RESPONSE_DESCRIPTION, responseDescription);
            invokeLogger(auditLogger::info, ApplicationMsgs.MESSAGE_AUDIT, args);
        }
    }

    /**
     * Log an audit message to the audit logger representing a non-specific processing success message.
     *
     * @param msg
     */
    public void logAuditSuccess(String msg) {
        Status status = Status.OK;
        logAudit(StatusCode.COMPLETE, Integer.toString(status.getStatusCode()), status.getReasonPhrase(), msg);
    }

    /**
     * Log an audit message to the audit logger representing an internal error (e.g. for an exception thrown by the
     * implementation). This method is expected to be called when a generic error response is returned to the caller to
     * indicate a processing failure.
     *
     * @param e Exception
     */
    public void logAuditError(Exception e) {
        Status status = Status.INTERNAL_SERVER_ERROR;
        logAudit(StatusCode.ERROR, Integer.toString(status.getStatusCode()), status.getReasonPhrase(), e.getMessage());
    }

    /**
     * Log a message to the metrics log.
     *
     * @param error the log code
     * @param args the info messages
     */
    public void logMetrics(final String... args) {
        if (metricsLogger.isInfoEnabled()) {
            invokeLogger(metricsLogger::info, ApplicationMsgs.MESSAGE_METRIC, args);
        }
    }

    /**
     * @param stopwatch
     * @param args
     */
    public void logMetrics(final StopWatch stopwatch, String... args) {
        setMdcElapsedTime(stopwatch);
        logMetrics(args);
    }

    @Override
    public String formatMsg(@SuppressWarnings("rawtypes") Enum arg0, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isDebugEnabled() {
        return debugLogger != null && debugLogger.isDebugEnabled();
    }

    @Override
    public boolean isErrorEnabled() {
        return errorLogger.isErrorEnabled();
    }

    @Override
    public boolean isInfoEnabled() {
        return errorLogger.isInfoEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return debugLogger.isTraceEnabled();
    }

    @Override
    public boolean isWarnEnabled() {
        return errorLogger.isWarnEnabled();
    }

    /**
     * Log a DEBUG level message to the debug logger.
     *
     * @param message the debug message
     */
    @Override
    public void debug(String message) {
        if (isDebugEnabled()) {
            invokeLogger(debugLogger::debug, message);
        }
    }

    @Override
    public void debug(@SuppressWarnings("rawtypes") Enum errorCode, String... args) {
        if (isDebugEnabled()) {
            invokeErrorCodeLogger(debugLogger::debug, (EELFResolvableErrorEnum) errorCode, args);
        }
    }

    @Override
    public void debug(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void error(@SuppressWarnings("rawtypes") Enum errorCode, String... args) {
        if (isErrorEnabled()) {
            invokeErrorCodeLogger(errorLogger::error, (EELFResolvableErrorEnum) errorCode, args);
        }
    }

    @Override
    public void error(@SuppressWarnings("rawtypes") Enum errorCode, Throwable t, String... args) {
        if (isErrorEnabled()) {
            invokeErrorCodeLogger(errorLogger::error, (EELFResolvableErrorEnum) errorCode, t, args);
        }
    }

    @Override
    public void error(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void error(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, Throwable arg2, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void info(@SuppressWarnings("rawtypes") Enum errorCode, String... args) {
        if (isInfoEnabled()) {
            invokeErrorCodeLogger(errorLogger::info, (EELFResolvableErrorEnum) errorCode, args);
        }
    }

    @Override
    public void info(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void info(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, MdcOverride arg2, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void trace(@SuppressWarnings("rawtypes") Enum errorCode, String... args) {
        if (isTraceEnabled()) {
            invokeErrorCodeLogger(debugLogger::trace, (EELFResolvableErrorEnum) errorCode, args);
        }
    }

    @Override
    public void trace(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void warn(@SuppressWarnings("rawtypes") Enum errorCode, String... args) {
        if (isWarnEnabled()) {
            invokeErrorCodeLogger(errorLogger::warn, (EELFResolvableErrorEnum) errorCode, args);
        }
    }

    @Override
    public void warn(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) {
        throw new UnsupportedOperationException();
    }

    /**
     * Get the method name for a calling method (from the current stack trace)
     *
     * @param level number of levels for the caller (not including this method)
     * @return the class and name of the calling method in the form "class#method"
     */
    public static String getCallerMethodName(int level) {
        StackTraceElement callingMethod = Thread.currentThread().getStackTrace()[level + 2];
        return callingMethod.getClassName() + "#" + callingMethod.getMethodName();
    }

    /**
     * Convenience method to be used only for testing purposes.
     *
     * @return the directory storing the log files
     */
    public static String getLogDirectory() {
        ch.qos.logback.classic.Logger logger =
                (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger("com.att.eelf");
        AsyncAppender appender = (AsyncAppender) logger.getAppender("asyncEELF");
        FileAppender<?> fileAppender = (FileAppender<?>) appender.getAppender("EELF");
        return new File(fileAppender.getFile()).getParent();
    }

    private void setMdcClassName() {
        MDC.put(MdcParameter.CLASS_NAME.value(), getCallerMethodName(3));
    }

    private void unsetMdcClassName() {
        MDC.put(MdcParameter.CLASS_NAME.value(), null);
    }

    /**
     * Set the Begin, End, and Elapsed time values in the MDC context.
     *
     * @param stopwatch
     */
    private void setMdcElapsedTime(final StopWatch stopwatch) {
        long startTime = stopwatch.getStartTime();
        long elapsedTime = stopwatch.getTime();

        setContextValue(MdcParameter.BEGIN_TIMESTAMP, timestampFormat.format(startTime));
        setContextValue(MdcParameter.END_TIMESTAMP, timestampFormat.format(startTime + elapsedTime));
        setContextValue(MdcParameter.ELAPSED_TIME, Long.toString(elapsedTime)); // Milliseconds
    }

    /**
     * @param logMethod the logger method to invoke
     * @param message
     */
    private void invokeLogger(Consumer<String> logMethod, String message) {
        setMdcClassName();
        logMethod.accept(message);
        unsetMdcClassName();
    }

    /**
     * @param logMethod
     * @param msg
     * @param args
     */
    private void invokeLogger(BiConsumer<ApplicationMsgs, String[]> logMethod, ApplicationMsgs msg, String[] args) {
        setMdcClassName();
        logMethod.accept(msg, args);
        unsetMdcClassName();
    }

    /**
     * @param logMethod
     * @param errorEnum
     * @param args
     */
    private void invokeErrorCodeLogger(BiConsumer<EELFResolvableErrorEnum, String[]> logMethod,
            EELFResolvableErrorEnum errorEnum, String[] args) {
        setMdcClassName();
        logMethod.accept(errorEnum, args);
        unsetMdcClassName();
    }

    /**
     * @param logMethod
     * @param errorEnum
     * @param t a Throwable
     * @param args
     */
    private void invokeErrorCodeLogger(TriConsumer<EELFResolvableErrorEnum, Throwable, String[]> logMethod,
            EELFResolvableErrorEnum errorEnum, Throwable t, String[] args) {
        setMdcClassName();
        logMethod.accept(errorEnum, t, args);
        unsetMdcClassName();
    }
}