I am trying to log exceptions that come from IKVM.OpenJDK.Jdbc assembly. One exception that occurs is java.sql.SQLException
(and subclasses). log4Net seems to trip up over this because java.sql.SQLException
implements IEnumerable
and the implementation of that just returns itself. I have stepped through the log4net code for this and it looks like log4net says something like:
"Here is a SQLException to log. Does it contain any information that I should log? Oh, yes it does. There is an IEnumerable, I had better log the stuff inside the IEnumerable. So what is in that IEnumerable? Oh, look it is a SQLException to log. Does it contain any that I should log? Oh, yes it does. There is an IEnumerable, I had better log the stuff inside the IEnumerable. So what is in that IEnumerable? Oh, look it is a SQLException..."
Eventually we get a StackOverflowException
.
Has anybody had and solved this problem before?
Minimal Verifiable Complete Example:
static void Main(string[] args)
{
java.sql.SQLException ex = new java.sql.SQLException();
log4net.Config.BasicConfigurator.Configure();
log4net.ILog logger = log4net.LogManager.GetLogger("foo");
logger.Error("This exception overflows the stack -> ", ex); // Exception here
Console.WriteLine("Finished. Press any key...");
Console.ReadKey();
}
And the packages.config I used to get the right NuGets to make it compile (NuGet source = https://www.nuget.org/api/v2/):
<packages>
<package id="log4net" version="2.0.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Jdbc" version="7.2.4630.5" targetFramework="net452" />
<!-- The packages below are dependencies from manually getting the packages above. -->
<package id="IKVM.OpenJDK.Charsets" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Core" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Misc" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Naming" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Remoting" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Security" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.SwingAWT" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Text" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.Util" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.OpenJDK.XML.API" version="7.2.4630.5" targetFramework="net452" />
<package id="IKVM.Runtime" version="7.2.4630.5" targetFramework="net452" />
</packages>
Having an exception implement IEnumerable
is arguably a design error -- in .NET, this is implemented through Exception.InnerException
for the general case and SqlException.Errors
for database errors in particular. However, filing a bug report will probably do you no good since IKVM's goal is to implement Java in .NET, so it will want to stick close to its roots.
Filing a bug report with log4net has a bit more chance of succeeding, though not much more, since handling this in a fully general manner would require cycle detection in enumerables, which is a massive pain.
Fortunately there's a simpler workaround; log4net is highly customizable, and through the use of a custom object renderer we can do with these exceptions whatever we like. Since the oddity of enumerable exceptions seems to be confined to IKVM, we can probably get away with making it specific to Throwable
:
class ThrowableRenderer : log4net.ObjectRenderer.IObjectRenderer {
private readonly IObjectRenderer fallback;
public ThrowableRenderer(RendererMap rendererMap) {
this.fallback = rendererMap.Get(typeof(Throwable));
}
public void RenderObject(RendererMap rendererMap, object obj, TextWriter writer) {
var filteredEnumerable = (obj as IEnumerable)?.Cast<object>().Skip(1);
if (filteredEnumerable != null) {
rendererMap.FindAndRender(obj.ToString(), writer);
if (filteredEnumerable.Any()) {
writer.WriteLine();
rendererMap.FindAndRender(filteredEnumerable, writer);
}
} else {
fallback.RenderObject(rendererMap, obj, writer);
}
}
}
And then you configure your custom renderer like so:
var rendererMap = log4net.LogManager.GetRepository().RendererMap;
rendererMap.Put(typeof(Throwable), new ThrowableRenderer(rendererMap));
In the above implementation, I have customized rendering of Throwable
s as first rendering the Throwable
itself as a flat object, and then any nested exceptions in the chain, but not the first exception itself. You may wish to customize this further, because, unlike a standard Exception
, Throwable.ToString()
does not appear to include the stack trace (while you can have log4net add the stack trace to any error message, retrieving this stack trace is much slower), but this demonstrates the basic idea of avoiding a stack overflow by sidestepping the default rendering.