// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Net.Http;
using System.Web.Http.Tracing.Properties;
namespace System.Web.Http.Tracing
{
///
/// Implementation of that traces to
///
public class SystemDiagnosticsTraceWriter : ITraceWriter
{
// Duplicate of internal category name traced by WebApi for start/end of request
private const string SystemWebHttpRequestCategory = "System.Web.Http.Request";
private static readonly TraceEventType[] TraceLevelToTraceEventType = new TraceEventType[]
{
// TraceLevel.Off
(TraceEventType)0,
// TraceLevel.Debug
TraceEventType.Verbose,
// TraceLevel.Info
TraceEventType.Information,
// TraceLevel.Warn
TraceEventType.Warning,
// TraceLevel.Error
TraceEventType.Error,
// TraceLevel.Fatal
TraceEventType.Critical
};
private TraceLevel _minLevel = TraceLevel.Info;
///
/// Gets or sets the minimum trace level.
///
///
/// Any below this
/// level will be ignored. The default for this property
/// is .
///
public TraceLevel MinimumLevel
{
get
{
return _minLevel;
}
set
{
if (value < TraceLevel.Off || value > TraceLevel.Fatal)
{
throw Error.ArgumentOutOfRange("value",
value,
SRResources.TraceLevelOutOfRange);
}
_minLevel = value;
}
}
///
/// Gets or sets a value indicating whether the formatted message
/// should be the verbose format, meaning it displays all fields
/// of the .
///
/// true means all fields
/// will be traced, false means only minimal information
/// will be traced. The default value is false.
public bool IsVerbose { get; set; }
///
/// Gets or sets the to which the
/// traces will be sent.
///
///
/// This property allows a custom
/// to be used when writing the traces.
/// This allows an application to configure and use its
/// own other than the default
/// .
/// If the value is null, this trace writer will
/// send traces to .
///
public TraceSource TraceSource { get; set; }
///
/// Writes a trace to if the
/// is greater than or equal .
///
/// The associated with this trace.
/// It may be null but the resulting trace will contain no correlation ID.
/// The category for the trace. This can be any user-defined
/// value. It is not interpreted by this implementation but is written to the trace.
/// The of this trace. If it is less than
/// , this trace request will be ignored.
/// The user callback to invoke to fill in a
/// with additional information to add to the trace.
public virtual void Trace(HttpRequestMessage request, string category, TraceLevel level, Action traceAction)
{
if (category == null)
{
throw Error.ArgumentNull("category");
}
if (traceAction == null)
{
throw Error.ArgumentNull("traceAction");
}
if (level < TraceLevel.Off || level > TraceLevel.Fatal)
{
throw Error.ArgumentOutOfRange("level",
level,
SRResources.TraceLevelOutOfRange);
}
if (MinimumLevel == TraceLevel.Off || level < MinimumLevel)
{
return;
}
TraceRecord traceRecord = new TraceRecord(request, category, level);
traceAction(traceRecord);
TranslateHttpResponseException(traceRecord);
string message = Format(traceRecord);
if (!String.IsNullOrEmpty(message))
{
// Level may have changed in Translate above
TraceMessage(traceRecord.Level, message);
}
}
///
/// Formats the contents of the given into
/// a single string containing comma-separated name-value pairs
/// for each property.
///
/// The from which
/// to produce the result.
/// A string containing comma-separated name-value pairs.
public virtual string Format(TraceRecord traceRecord)
{
if (traceRecord == null)
{
throw Error.ArgumentNull("traceRecord");
}
// The first and last traces are injected by the tracing system itself.
// We use these to format unique strings identifying the incoming request
// and the outgoing response.
if (String.Equals(traceRecord.Category, SystemWebHttpRequestCategory, StringComparison.Ordinal))
{
return FormatRequestEnvelope(traceRecord);
}
List messages = new List();
if (!IsVerbose)
{
// In short format mode, we trace only End traces because it is
// where the results of each operation will appear.
if (traceRecord.Kind == TraceKind.Begin)
{
return null;
}
}
else
{
messages.Add(Error.Format(SRResources.TimeLevelKindFormat,
FormatDateTime(traceRecord.Timestamp),
traceRecord.Level.ToString(),
traceRecord.Kind.ToString()));
if (!String.IsNullOrEmpty(traceRecord.Category))
{
messages.Add(Error.Format(SRResources.CategoryFormat, traceRecord.Category));
}
messages.Add(Error.Format(SRResources.IdFormat, traceRecord.RequestId.ToString()));
}
if (!String.IsNullOrEmpty(traceRecord.Message))
{
messages.Add(Error.Format(SRResources.MessageFormat, traceRecord.Message));
}
if (traceRecord.Operator != null || traceRecord.Operation != null)
{
messages.Add(Error.Format(SRResources.OperationFormat, traceRecord.Operator, traceRecord.Operation));
}
if (traceRecord.Status != 0)
{
messages.Add(Error.Format(SRResources.HttpStatusFormat, (int)traceRecord.Status, traceRecord.Status.ToString()));
}
if (traceRecord.Exception != null)
{
messages.Add(Error.Format(SRResources.ExceptionFormat, traceRecord.Exception.ToString()));
}
return String.Join(", ", messages);
}
///
/// Formats the given into a string describing
/// either the initial receipt of the incoming request or the final send
/// of the response, depending on .
///
/// The from which to
/// produce the result.
/// A string containing comma-separated name-value pairs.
public virtual string FormatRequestEnvelope(TraceRecord traceRecord)
{
if (traceRecord == null)
{
throw Error.ArgumentNull("traceRecord");
}
List messages = new List();
if (IsVerbose)
{
messages.Add(Error.Format((traceRecord.Kind == TraceKind.Begin)
? SRResources.TimeRequestFormat
: SRResources.TimeResponseFormat,
FormatDateTime(traceRecord.Timestamp)));
}
else
{
messages.Add((traceRecord.Kind == TraceKind.Begin)
? SRResources.ShortRequestFormat
: SRResources.ShortResponseFormat);
}
if (traceRecord.Status != 0)
{
messages.Add(Error.Format(SRResources.HttpStatusFormat, (int)traceRecord.Status, traceRecord.Status.ToString()));
}
if (traceRecord.Request != null)
{
messages.Add(Error.Format(SRResources.HttpMethodFormat, traceRecord.Request.Method));
if (traceRecord.Request.RequestUri != null)
{
messages.Add(Error.Format(SRResources.UrlFormat, traceRecord.Request.RequestUri.ToString()));
}
}
if (IsVerbose)
{
messages.Add(Error.Format(SRResources.IdFormat, traceRecord.RequestId.ToString()));
}
// The Message and Exception fields do not contain interesting information unless
// there is a problem, so they appear after the more informative trace information.
if (!String.IsNullOrEmpty(traceRecord.Message))
{
messages.Add(Error.Format(SRResources.MessageFormat, traceRecord.Message));
}
if (traceRecord.Exception != null)
{
messages.Add(Error.Format(SRResources.ExceptionFormat, traceRecord.Exception.ToString()));
}
return String.Join(", ", messages);
}
///
/// Examines the given to determine whether it
/// contains an and if so, modifies
/// the to capture more detailed information.
///
/// The to examine and modify.
public virtual void TranslateHttpResponseException(TraceRecord traceRecord)
{
if (traceRecord == null)
{
throw Error.ArgumentNull("traceRecord");
}
TraceWriterExceptionMapper.TranslateHttpResponseException(traceRecord);
}
///
/// Formats a for the trace.
///
///
/// The default implementation uses the ISO 8601 convention
/// for round-trippable dates so they can be parsed.
///
/// The
/// The formatted as a string
public virtual string FormatDateTime(DateTime dateTime)
{
// The 'o' format is ISO 8601 for a round-trippable DateTime.
// It is culture-invariant and can be parsed.
return dateTime.ToString("o", CultureInfo.InvariantCulture);
}
private void TraceMessage(TraceLevel level, string message)
{
Contract.Assert(level >= TraceLevel.Off && level <= TraceLevel.Fatal);
// If the user registered a custom TraceSource, we write a trace event to it
// directly, preserving the event type.
TraceSource traceSource = TraceSource;
if (traceSource != null)
{
traceSource.TraceEvent(eventType: TraceLevelToTraceEventType[(int)level], id: 0, message: message);
return;
}
// If there is no custom TraceSource, trace to System.Diagnostics.Trace.
// But System.Diagnostics.Trace does not offer a public API to trace
// TraceEventType.Verbose or TraceEventType.Critical, meaning our
// TraceLevel.Debug and TraceLevel.Fatal cannot be report directly.
// Consequently, we translate Verbose to Trace.WriteLine and
// Critical to TraceEventType.Error.
// Windows Azure Diagnostics' TraceListener already translates
// a WriteLine to a Verbose, so on Azure the Debug trace will be
// handled properly.
switch (level)
{
case TraceLevel.Off:
return;
case TraceLevel.Debug:
System.Diagnostics.Trace.WriteLine(message);
return;
case TraceLevel.Info:
System.Diagnostics.Trace.TraceInformation(message);
return;
case TraceLevel.Warn:
System.Diagnostics.Trace.TraceWarning(message);
return;
case TraceLevel.Error:
case TraceLevel.Fatal:
System.Diagnostics.Trace.TraceError(message);
return;
}
}
}
}