blob: 50394facf5c6cf0dbddc13cb752dabe36c69c1a7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
namespace Apache.Ignite.Core.Tests.Log
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Apache.Ignite.Core.Cache.Configuration;
using Apache.Ignite.Core.Common;
using Apache.Ignite.Core.Communication.Tcp;
using Apache.Ignite.Core.Compute;
using Apache.Ignite.Core.Lifecycle;
using Apache.Ignite.Core.Log;
using Apache.Ignite.Core.Resource;
using NUnit.Framework;
/// <summary>
/// Tests that user-defined logger receives Ignite events.
/// </summary>
public class CustomLoggerTest
{
/** */
private static readonly LogLevel[] AllLevels = Enum.GetValues(typeof (LogLevel)).OfType<LogLevel>().ToArray();
/// <summary>
/// Test setup.
/// </summary>
[SetUp]
public void TestSetUp()
{
TestLogger.Clear();
}
/// <summary>
/// Tests the startup output.
/// </summary>
[Test]
public void TestStartupOutput()
{
var cfg = GetConfigWithLogger(true);
using (var ignite = Ignition.Start(cfg))
{
// Check injection
Assert.AreEqual(ignite, ((TestLogger) cfg.Logger).Ignite);
// Check initial message
Assert.IsTrue(TestLogger.Entries[0].Message.StartsWith("Starting Ignite.NET"));
// Check topology message
Assert.IsTrue(
TestUtils.WaitForCondition(() =>
{
lock (TestLogger.Entries)
{
return TestLogger.Entries.Any(x => x.Message.Contains("Topology snapshot"));
}
}, 9000), "No topology snapshot");
}
// Test that all levels are present
foreach (var level in AllLevels.Where(x => x != LogLevel.Error))
{
Assert.IsTrue(TestLogger.Entries.Any(x => x.Level == level), "No messages with level " + level);
}
}
/// <summary>
/// Tests startup error in Java.
/// </summary>
[Test]
public void TestStartupJavaError()
{
// Invalid config
Assert.Throws<IgniteException>(() =>
Ignition.Start(new IgniteConfiguration(GetConfigWithLogger())
{
CommunicationSpi = new TcpCommunicationSpi
{
IdleConnectionTimeout = TimeSpan.MinValue
}
}));
var err = TestLogger.Entries.First(x => x.Level == LogLevel.Error);
Assert.IsTrue(err.NativeErrorInfo.Contains("SPI parameter failed condition check: idleConnTimeout > 0"));
Assert.AreEqual("org.apache.ignite.internal.IgniteKernal", err.Category);
Assert.IsNull(err.Exception);
}
/// <summary>
/// Tests startup error in .NET.
/// </summary>
[Test]
public void TestStartupDotNetError()
{
// Invalid bean
Assert.Throws<IgniteException>(() =>
Ignition.Start(new IgniteConfiguration(GetConfigWithLogger())
{
LifecycleHandlers = new[] {new FailBean()}
}));
var err = TestLogger.Entries.First(x => x.Level == LogLevel.Error);
Assert.IsInstanceOf<ArithmeticException>(err.Exception);
}
/// <summary>
/// Tests that .NET exception propagates through Java to the log.
/// </summary>
[Test]
public void TestDotNetErrorPropagation()
{
// Start 2 nodes: PlatformNativeException does not occur in local scenario
using (var ignite = Ignition.Start(GetConfigWithLogger()))
using (Ignition.Start(new IgniteConfiguration(TestUtils.GetTestConfiguration()) {IgniteInstanceName = "1"}))
{
var compute = ignite.GetCluster().ForRemotes().GetCompute();
var ex = Assert.Throws<AggregateException>(() => compute.Call(new FailFunc()));
Assert.IsNotNull(ex.InnerException);
Assert.IsInstanceOf<ArithmeticException>(ex.InnerException.InnerException);
// Log updates may not arrive immediately
TestUtils.WaitForCondition(() => TestLogger.Entries.Any(x => x.Exception != null), 3000);
var errFromJava = TestLogger.Entries.Single(x => x.Exception != null);
Assert.IsNotNull(errFromJava.Exception.InnerException);
Assert.AreEqual("Error in func.",
((ArithmeticException) errFromJava.Exception.InnerException).Message);
}
}
/// <summary>
/// Tests the <see cref="QueryEntity"/> validation.
/// </summary>
[Test]
public void TestQueryEntityValidation()
{
var cacheCfg = new CacheConfiguration("cache1", new QueryEntity(typeof(uint), typeof(ulong))
{
Fields = new[]
{
new QueryField("myField", typeof(ushort))
}
});
var cfg = new IgniteConfiguration(GetConfigWithLogger())
{
CacheConfiguration = new[]
{
cacheCfg
}
};
using (var ignite = Ignition.Start(cfg))
{
// Check static and dynamic cache start
cacheCfg.Name = "cache2";
ignite.CreateCache<int, string>(cacheCfg);
var warns = TestLogger.Entries.Where(x => x.Level == LogLevel.Warn && x.Args != null)
.Select(x => string.Format(x.Message, x.Args)).ToList();
Assert.AreEqual(6, warns.Count);
Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
"Long': Type 'System.UInt32' maps to Java type 'java.lang.Integer' using unchecked " +
"conversion. This may cause issues in SQL queries. You can use 'System.Int32' " +
"instead to achieve direct mapping.", warns[0]);
Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
"Long': Type 'System.UInt64' maps to Java type 'java.lang.Long' using unchecked " +
"conversion. This may cause issues in SQL queries. You can use 'System.Int64' " +
"instead to achieve direct mapping.", warns[1]);
Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
"Long', QueryField 'myField': Type 'System.UInt16' maps to Java type 'java.lang." +
"Short' using unchecked conversion. This may cause issues in SQL queries. You " +
"can use 'System.Int16' instead to achieve direct mapping.", warns[2]);
Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
"Long': Type 'System.UInt32' maps to Java type 'java.lang.Integer' using unchecked " +
"conversion. This may cause issues in SQL queries. You can use 'System.Int32' " +
"instead to achieve direct mapping.", warns[3]);
Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
"Long': Type 'System.UInt64' maps to Java type 'java.lang.Long' using unchecked " +
"conversion. This may cause issues in SQL queries. You can use 'System.Int64' " +
"instead to achieve direct mapping.", warns[4]);
Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
"Long', QueryField 'myField': Type 'System.UInt16' maps to Java type 'java.lang." +
"Short' using unchecked conversion. This may cause issues in SQL queries. You " +
"can use 'System.Int16' instead to achieve direct mapping.", warns[5]);
}
}
/// <summary>
/// Tests the nullable <see cref="QueryEntity"/> validation.
/// </summary>
[TestCase(typeof(sbyte?), "java.lang.Byte", typeof(byte))]
[TestCase(typeof(ushort?), "java.lang.Short", typeof(short))]
[TestCase(typeof(uint?), "java.lang.Integer", typeof(int))]
[TestCase(typeof(ulong?), "java.lang.Long", typeof(long))]
public void TestNullableQueryEntityValidation(Type type, string javaType, Type hintType)
{
var cfg = new IgniteConfiguration(GetConfigWithLogger())
{
CacheConfiguration = new[]
{
new CacheConfiguration("cache1", new QueryEntity(type, type)
{
Fields = new[]
{
new QueryField("myField", type)
}
})
}
};
using (Ignition.Start(cfg))
{
var warns = TestLogger.Entries.Where(x => x.Level == LogLevel.Warn && x.Args != null)
.Select(x => string.Format(x.Message, x.Args)).ToList();
Assert.AreEqual(3, warns.Count);
string pattern = "Validating cache configuration 'cache1', QueryEntity '{0}:{0}': " +
"Type '{1}' maps to Java type '{0}' using unchecked " +
"conversion. This may cause issues in SQL queries. You can use '{2}' " +
"instead to achieve direct mapping.";
string expected = string.Format(pattern, javaType, type, hintType);
Assert.AreEqual(expected, warns[0]);
Assert.AreEqual(expected, warns[1]);
string fieldsPattern = "Validating cache configuration 'cache1', QueryEntity '{0}:{0}', " +
"QueryField 'myField': Type '{1}' maps to Java type '{0}' using " +
"unchecked conversion. This may cause issues in SQL queries. " +
"You can use '{2}' instead to achieve direct mapping.";
string fieldsExpected = string.Format(fieldsPattern, javaType, type, hintType);
Assert.AreEqual(fieldsExpected, warns[2]);
}
}
/// <summary>
/// Tests cache creation with nullable <see cref="QueryEntity"/> does not log warnings.
/// </summary>
[TestCase(typeof(int))]
[TestCase(typeof(int?))]
[TestCase(typeof(string))]
[TestCase(typeof(LogEntry))]
[TestCase(typeof(CustomLoggerTest))]
[TestCase(typeof(CustomEnum))]
[TestCase(typeof(CustomEnum?))]
public void TestCacheCreationWithDirectQueryEntityMappingsDoesNotLogWarnings(Type type)
{
var cfg = new IgniteConfiguration(GetConfigWithLogger())
{
CacheConfiguration = new[]
{
new CacheConfiguration("cache1", new QueryEntity(type, type)
{
Fields = new[]
{
new QueryField("myField", type)
}
})
}
};
using (Ignition.Start(cfg))
{
int warnsCount = TestLogger.Entries.Count(x => x.Level == LogLevel.Warn && x.Args != null);
Assert.AreEqual(0, warnsCount);
}
}
/// <summary>
/// Tests the <see cref="LoggerExtensions"/> methods.
/// </summary>
[Test]
public void TestExtensions()
{
var log = new TestLogger(LogLevel.Trace);
var ex = new FieldAccessException("abc");
// Log
log.Log(LogLevel.Trace, "trace");
CheckLastMessage(LogLevel.Trace, "trace");
log.Log(LogLevel.Debug, "msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Debug, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Log(LogLevel.Info, ex, "msg");
CheckLastMessage(LogLevel.Info, "msg", e: ex);
log.Log(LogLevel.Warn, ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Warn, "msg {0}", new object[] {1}, CultureInfo.InvariantCulture, e: ex);
// Trace
log.Trace("trace");
CheckLastMessage(LogLevel.Trace, "trace");
log.Trace("msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Trace, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Trace(ex, "msg");
CheckLastMessage(LogLevel.Trace, "msg", e: ex);
log.Trace(ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Trace, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);
// Debug
log.Debug("test");
CheckLastMessage(LogLevel.Debug, "test");
log.Debug("msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Debug, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Debug(ex, "msg");
CheckLastMessage(LogLevel.Debug, "msg", e: ex);
log.Debug(ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Debug, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);
// Info
log.Info("test");
CheckLastMessage(LogLevel.Info, "test");
log.Info("msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Info, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Info(ex, "msg");
CheckLastMessage(LogLevel.Info, "msg", e: ex);
log.Info(ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Info, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);
// Warn
log.Warn("test");
CheckLastMessage(LogLevel.Warn, "test");
log.Warn("msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Warn, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Warn(ex, "msg");
CheckLastMessage(LogLevel.Warn, "msg", e: ex);
log.Warn(ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Warn, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);
// Error
log.Error("test");
CheckLastMessage(LogLevel.Error, "test");
log.Error("msg {0} {1}", 1, "2");
CheckLastMessage(LogLevel.Error, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);
log.Error(ex, "msg");
CheckLastMessage(LogLevel.Error, "msg", e: ex);
log.Error(ex, "msg {0}", 1);
CheckLastMessage(LogLevel.Error, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);
// GetLogger
var catLog = log.GetLogger("myCategory");
catLog.Info("info");
CheckLastMessage(LogLevel.Info, "info", category: "myCategory");
catLog.Log(LogLevel.Info, "info", null, null, "explicitCat", null, null);
CheckLastMessage(LogLevel.Info, "info", category: "explicitCat");
catLog = catLog.GetLogger("newCat");
catLog.Info("info");
CheckLastMessage(LogLevel.Info, "info", category: "newCat");
catLog.Log(LogLevel.Info, "info", null, null, "explicitCat", null, null);
CheckLastMessage(LogLevel.Info, "info", category: "explicitCat");
}
/// <summary>
/// Checks the last message.
/// </summary>
private static void CheckLastMessage(LogLevel level, string message, object[] args = null,
IFormatProvider formatProvider = null, string category = null, string nativeErr = null, Exception e = null)
{
var msg = TestLogger.Entries.Last();
Assert.AreEqual(msg.Level, level);
Assert.AreEqual(msg.Message, message);
Assert.AreEqual(msg.Args, args);
Assert.AreEqual(msg.FormatProvider, formatProvider);
Assert.AreEqual(msg.Category, category);
Assert.AreEqual(msg.NativeErrorInfo, nativeErr);
Assert.AreEqual(msg.Exception, e);
}
/// <summary>
/// Gets the configuration with logger.
/// </summary>
private static IgniteConfiguration GetConfigWithLogger(bool verbose = false)
{
return new IgniteConfiguration(TestUtils.GetTestConfiguration())
{
Logger = new TestLogger(verbose ? LogLevel.Trace : LogLevel.Info)
};
}
/// <summary>
/// Test log entry.
/// </summary>
private class LogEntry
{
public LogLevel Level;
public string Message;
public object[] Args;
public IFormatProvider FormatProvider;
public string Category;
public string NativeErrorInfo;
public Exception Exception;
public override string ToString()
{
return string.Format("Level: {0}, Message: {1}, Args: {2}, FormatProvider: {3}, Category: {4}, " +
"NativeErrorInfo: {5}, Exception: {6}", Level, Message, Args, FormatProvider,
Category, NativeErrorInfo, Exception);
}
}
/// <summary>
/// Test logger.
/// </summary>
private class TestLogger : ILogger
{
private static readonly List<LogEntry> Logs = new List<LogEntry>(5000);
private readonly LogLevel _minLevel;
public TestLogger(LogLevel minLevel)
{
_minLevel = minLevel;
}
public static List<LogEntry> Entries
{
get
{
lock (Logs)
{
return Logs.ToList();
}
}
}
public void Log(LogLevel level, string message, object[] args, IFormatProvider formatProvider,
string category, string nativeErrorInfo, Exception ex)
{
if (!IsEnabled(level))
return;
lock (Logs)
{
Logs.Add(new LogEntry
{
Level = level,
Message = message,
Args = args,
FormatProvider = formatProvider,
Category = category,
NativeErrorInfo = nativeErrorInfo,
Exception = ex
});
}
}
public bool IsEnabled(LogLevel level)
{
return level >= _minLevel;
}
[InstanceResource]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public IIgnite Ignite { get; set; }
public static void Clear()
{
lock (Logs)
{
Logs.Clear();
}
}
}
/// <summary>
/// Failing lifecycle bean.
/// </summary>
private class FailBean : ILifecycleHandler
{
public void OnLifecycleEvent(LifecycleEventType evt)
{
throw new ArithmeticException("Failure in bean");
}
}
/// <summary>
/// Failing computation.
/// </summary>
[Serializable]
private class FailFunc : IComputeFunc<string>
{
public string Invoke()
{
throw new ArithmeticException("Error in func.");
}
}
/// <summary>
/// Custom enum for testing.
/// </summary>
private struct CustomEnum
{
// ReSharper disable once UnusedMember.Local
public int Field { get; set; }
}
}
}