// Copyright 2014 Google Inc. All Rights Reserved. // // 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. package com.google.api.ads.adwords.lib.utils.logging; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.util.Data; import com.google.api.client.util.GenericData; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import com.google.inject.name.Named; import org.slf4j.Logger; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.inject.Inject; /** * Logger that logs report requests and responses according to the following rules.<br/> * <ul> * <li>Log successful requests (header and payload) and responses to INFO.</li> * <li>Log failed requests (header and payload) and responses to WARN.</li> * </ul> */ public class ReportServiceLogger { private final Logger reportLogger; /** * Headers whose value should not be logged because they contain sensitive information. * Entries are in lowercase, but scrubbing should be case-insensitive. */ @VisibleForTesting static final Set<String> SCRUBBED_HEADERS = Sets.newHashSet("authorization", "authtoken", "password", "developertoken"); @VisibleForTesting static final String SCRUBBED_HEADERS_VALUE = "REDACTED"; /** * Constructor that takes an injected logger identified by name. * * @param reportLogger underlying SLF4J logger for report service interactions */ @Inject ReportServiceLogger(@Named(AdWordsLoggingModule.REPORT_LOGGER_NAME) Logger reportLogger) { this.reportLogger = reportLogger; } /** * Logs the request at the proper log level. */ public void logRequest(String requestMethod, GenericUrl url, HttpContent requestContent, GenericData requestHeaders, boolean isSuccessful) { if (!isLoggable(isSuccessful)) { return; } log(String.format("Request made: %s %s%n", requestMethod, url), isSuccessful); StringBuilder messageBuilder = new StringBuilder(); // Log headers. if (requestHeaders != null) { messageBuilder.append(getMapAsString(requestHeaders)); } // Log request parameters. messageBuilder.append(String.format("%nParameters:%n")); if (requestContent instanceof UrlEncodedContent) { UrlEncodedContent encodedContent = (UrlEncodedContent) requestContent; messageBuilder.append(getMapAsString(Data.mapOf(encodedContent.getData()))); } else if (requestContent != null) { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try { requestContent.writeTo(byteStream); messageBuilder.append(byteStream.toString()); } catch (IOException e) { messageBuilder.append("Unable to read request content due to exception: " + e); } } log(messageBuilder.toString(), isSuccessful); } /** * Logs the response at the proper log level based on its status code. */ public void logResponse(int statusCode, String statusMessage, boolean isSuccessful) { if (!isLoggable(isSuccessful)) { return; } String responseInfo = String.format("Response received with status code %d and message: %s%n", statusCode, statusMessage); log(responseInfo, isSuccessful); } /** * Returns the underlying logger for report service interactions. */ public Logger getLogger() { return reportLogger; } /** * Converts the map of key/value pairs to a multi-line string (one line per key). Masks sensitive * information for a predefined set of header keys. * * @param map a non-null Map * @return a non-null String */ private String getMapAsString(Map<String, Object> map) { StringBuilder messageBuilder = new StringBuilder(); for (Entry<String, Object> mapEntry : map.entrySet()) { Object headerValue = mapEntry.getValue(); // Perform a case-insensitive check if the header should be scrubbed. if (SCRUBBED_HEADERS.contains(mapEntry.getKey().toLowerCase())) { headerValue = SCRUBBED_HEADERS_VALUE; } messageBuilder.append(String.format("%s: %s%n", mapEntry.getKey(), headerValue)); } return messageBuilder.toString(); } private boolean isLoggable(boolean isSuccessful) { return isSuccessful ? reportLogger.isInfoEnabled() : reportLogger.isWarnEnabled(); } private void log(String message, boolean isSuccessful) { if (isSuccessful) { reportLogger.info(message); } else { reportLogger.warn(message); } } }