/**
* Copyright Microsoft Corporation
*
* 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.microsoft.azure.storage;
import static org.junit.Assert.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.slf4j.LoggerFactory;
import com.microsoft.azure.storage.TestRunners.CloudTests;
import com.microsoft.azure.storage.TestRunners.DevFabricTests;
import com.microsoft.azure.storage.TestRunners.DevStoreTests;
import com.microsoft.azure.storage.blob.BlobTestHelper;
import com.microsoft.azure.storage.blob.BlobType;
import com.microsoft.azure.storage.blob.CloudBlob;
import com.microsoft.azure.storage.blob.CloudBlobContainer;
import com.microsoft.azure.storage.core.LogConstants;
import com.microsoft.azure.storage.core.Logger;
/*
* If you'd like to use a different slf4j binding and/or not use Maven, you will need to add a different class path
* dependency and set the properties for it accordingly. The dependency and the properties file will need to be put in
* the appropriate locations for the logger implementation chosen.
*
* The log implemention/properties must:
* 1. Set the default/root log level to all.
* 2. Set the log level for the logger "limited" to error only.
* 3. Direct both loggers to log to System.err which these tests will redirect to a custom stream.
*
* Then, you will need to modify the readAndCompareOutput method to parse the logs entries accordingly.
*/
@Category({ DevFabricTests.class, DevStoreTests.class, CloudTests.class })
public class LoggerTests {
private final static String TRACE = "TRACE";
private final static String DEBUG = "DEBUG";
private final static String INFO = "INFO";
private final static String WARN = "WARN";
private final static String ERROR = "ERROR";
private final static String ARG0 = "Test string";
private final static String ARG1 = "Test string: arg1 = %s";
private final static String ARG2 = "Test string: arg1 = %s; arg2 = %s";
private final static String ARG3 = "Test string: arg1 = %s; arg2 = %s; arg3 = %s";
private final static String ARG1_VAL = "arg1_val";
private final static String ARG2_VAL1 = "arg2_val1";
private final static String ARG2_VAL2 = "arg2_val2";
private final static Object[] ARG3_VAL = { "arg3_val1", "arg3_val2", "arg3_val3" };
private final static String LIMITED_LOGGER_NAME = "limited";
private final static PrintStream old = System.err;
private static ByteArrayOutputStream baos;
@BeforeClass
public synchronized static void loggerTestsClassSetup() {
// redirect the error stream to a custom print stream
baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
System.setErr(ps);
}
@AfterClass
public synchronized static void loggerTestsClassTearDown() {
// flush the current error stream
System.err.flush();
// reset the error stream to what it was originally
System.setErr(old);
}
@Before
public synchronized void loggerTestsMethodSetup() {
// set default logging to off
OperationContext.setLoggingEnabledByDefault(false);
// clear the output stream
baos.reset();
}
@Test
public synchronized void testInheritedLogLevel() throws IOException {
// in the configs file, the default logger logs all levels
OperationContext.setLoggingEnabledByDefault(true);
OperationContext ctx = new OperationContext();
writeTraceLogs(ctx);
readAndCompareOutput(TRACE, OperationContext.defaultLoggerName, ctx.getClientRequestID());
writeDebugLogs(ctx);
readAndCompareOutput(DEBUG, OperationContext.defaultLoggerName, ctx.getClientRequestID());
writeInfoLogs(ctx);
readAndCompareOutput(INFO, OperationContext.defaultLoggerName, ctx.getClientRequestID());
writeWarnLogs(ctx);
readAndCompareOutput(WARN, OperationContext.defaultLoggerName, ctx.getClientRequestID());
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx.getClientRequestID());
// in the configs file, the limited logger logs only errors
ctx.setLogger(LoggerFactory.getLogger(LIMITED_LOGGER_NAME));
writeTraceLogs(ctx);
assertEquals(0, baos.toString().length());
writeDebugLogs(ctx);
assertEquals(0, baos.toString().length());
writeInfoLogs(ctx);
assertEquals(0, baos.toString().length());
writeWarnLogs(ctx);
assertEquals(0, baos.toString().length());
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, LIMITED_LOGGER_NAME, ctx.getClientRequestID());
}
@Test
public synchronized void testDefaultLogging() throws IOException {
// doesn't write logs by default
OperationContext ctx = new OperationContext();
writeErrorLogs(ctx);
assertEquals(0, baos.toString().length());
// set logging on by default and make sure a previous created context now logs
OperationContext.setLoggingEnabledByDefault(true);
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx.getClientRequestID());
// with logging set on by default, make sure a new context starts logging
OperationContext ctx2 = new OperationContext();
writeErrorLogs(ctx2);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx2.getClientRequestID());
// set logging off by default and make sure a previously created context stops logging
OperationContext.setLoggingEnabledByDefault(false);
writeErrorLogs(ctx);
assertEquals(0, baos.toString().length());
// with logging set off by default, make sure a new context doesn't logging
ctx2 = new OperationContext();
writeErrorLogs(ctx2);
assertEquals(0, baos.toString().length());
}
@Test
public synchronized void testRequestLevelLogging() throws IOException {
// enabling for an individual request works with default logger
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx.getClientRequestID());
// enabling for a previous request doesn't enable for newly created contexts
OperationContext ctx2 = new OperationContext();
writeErrorLogs(ctx2);
assertEquals(0, baos.toString().length());
// enabling for an individual request works with default logger
ctx.setLogger(LoggerFactory.getLogger(LIMITED_LOGGER_NAME));
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, LIMITED_LOGGER_NAME, ctx.getClientRequestID());
// changing logger for one request doesn't change logger for old request
ctx2.setLoggingEnabled(true);
writeErrorLogs(ctx2);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx2.getClientRequestID());
// changing logger for one request doen't change logger for new requests
ctx2 = new OperationContext();
ctx2.setLoggingEnabled(true);
writeErrorLogs(ctx2);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx2.getClientRequestID());
// turning logging off for a context doesn't change it's logger and doesn't log
ctx.setLoggingEnabled(false);
assertEquals(LIMITED_LOGGER_NAME, ctx.getLogger().getName());
writeErrorLogs(ctx);
assertEquals(0, baos.toString().length());
// turning logging on overall and off for a request works
OperationContext.setLoggingEnabledByDefault(true);
ctx.setLoggingEnabled(false);
writeErrorLogs(ctx);
assertEquals(0, baos.toString().length());
}
@Test
public synchronized void testTrace() throws IOException {
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeTraceLogs(ctx);
readAndCompareOutput(TRACE, OperationContext.defaultLoggerName, ctx.getClientRequestID());
}
@Test
public synchronized void testDebug() throws IOException {
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeDebugLogs(ctx);
readAndCompareOutput(DEBUG, OperationContext.defaultLoggerName, ctx.getClientRequestID());
}
@Test
public synchronized void testInfo() throws IOException {
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeInfoLogs(ctx);
readAndCompareOutput(INFO, OperationContext.defaultLoggerName, ctx.getClientRequestID());
}
@Test
public synchronized void testWarn() throws IOException {
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeWarnLogs(ctx);
readAndCompareOutput(WARN, OperationContext.defaultLoggerName, ctx.getClientRequestID());
}
@Test
public synchronized void testError() throws IOException {
OperationContext ctx = new OperationContext();
ctx.setLoggingEnabled(true);
writeErrorLogs(ctx);
readAndCompareOutput(ERROR, OperationContext.defaultLoggerName, ctx.getClientRequestID());
}
@Test
public synchronized void testStringToSign()
throws IOException, InvalidKeyException, StorageException, URISyntaxException {
OperationContext.setLoggingEnabledByDefault(true);
final CloudBlobContainer cont = BlobTestHelper.getRandomContainerReference();
try {
cont.create();
final CloudBlob blob = BlobTestHelper.uploadNewBlob(cont, BlobType.BLOCK_BLOB, "", 0, null);
// Test logging for SAS access
baos.reset();
blob.generateSharedAccessSignature(null, null);
baos.flush();
String log = baos.toString();
String[] logEntries = log.split("[\\r\\n]+");
assertEquals(1, logEntries.length);
// example log entry: TRACE ROOT - {0b902691-1a8e-41da-ab60-5b912df186a6}: {Test string}
String[] segment = logEntries[0].split("\\{");
assertEquals(3, segment.length);
assertTrue(segment[1].startsWith("*"));
assertTrue(segment[2].startsWith(String.format(LogConstants.SIGNING, Constants.EMPTY_STRING)));
baos.reset();
// Test logging for Shared Key access
OperationContext ctx = new OperationContext();
blob.downloadAttributes(null, null, ctx);
baos.flush();
log = baos.toString();
logEntries = log.split("[\\r\\n]+");
assertNotEquals(0, logEntries.length);
// Select correct log entry
for (int n = 0; n < logEntries.length; n++) {
if (logEntries[n].startsWith(LoggerTests.TRACE)) {
segment = logEntries[n].split("\\{");
assertEquals(3, segment.length);
assertTrue(segment[1].startsWith(ctx.getClientRequestID()));
assertTrue(segment[2].startsWith(String.format(LogConstants.SIGNING, Constants.EMPTY_STRING)));
return;
}
}
// If this line is reached then the log entry was not found
fail();
}
finally {
cont.deleteIfExists();
}
}
private void writeTraceLogs(OperationContext ctx) {
Logger.trace(ctx, ARG0);
Logger.trace(ctx, ARG1, ARG1_VAL);
Logger.trace(ctx, ARG2, ARG2_VAL1, ARG2_VAL2);
Logger.trace(ctx, ARG3, ARG3_VAL);
}
private void writeDebugLogs(OperationContext ctx) {
Logger.debug(ctx, ARG0);
Logger.debug(ctx, ARG1, ARG1_VAL);
Logger.debug(ctx, ARG2, ARG2_VAL1, ARG2_VAL2);
Logger.debug(ctx, ARG3, ARG3_VAL);
}
private void writeInfoLogs(OperationContext ctx) {
Logger.info(ctx, ARG0);
Logger.info(ctx, ARG1, ARG1_VAL);
Logger.info(ctx, ARG2, ARG2_VAL1, ARG2_VAL2);
Logger.info(ctx, ARG3, ARG3_VAL);
}
private void writeWarnLogs(OperationContext ctx) {
Logger.warn(ctx, ARG0);
Logger.warn(ctx, ARG1, ARG1_VAL);
Logger.warn(ctx, ARG2, ARG2_VAL1, ARG2_VAL2);
Logger.warn(ctx, ARG3, ARG3_VAL);
}
private void writeErrorLogs(OperationContext ctx) {
Logger.error(ctx, ARG0);
Logger.error(ctx, ARG1, ARG1_VAL);
Logger.error(ctx, ARG2, ARG2_VAL1, ARG2_VAL2);
Logger.error(ctx, ARG3, ARG3_VAL);
}
private void readAndCompareOutput(final String logLevel, final String loggerName, final String id)
throws IOException {
baos.flush();
String log = baos.toString();
String[] logEntries = log.split("[\\r\\n]+");
assertEquals(4, logEntries.length);
// example log entry: DEBUG ROOT - {0b902691-1a8e-41da-ab60-5b912df186a6}: {Test string}
String format = String.format("%s %s - {%s}: {%s}", logLevel, loggerName, id, "%s");
String log0 = ARG0;
String log1 = String.format(ARG1, ARG1_VAL);
String log2 = String.format(ARG2, ARG2_VAL1, ARG2_VAL2);
String log3 = String.format(ARG3, ARG3_VAL);
assertEquals(String.format(format, log0), logEntries[0]);
assertEquals(String.format(format, log1), logEntries[1]);
assertEquals(String.format(format, log2), logEntries[2]);
assertEquals(String.format(format, log3), logEntries[3]);
baos.reset();
}
}