I used TelemetryClient to sink my application logs in Application Insight. Now I want to control my logging according to the environment.
I tried to set LogLevel using host.json file but it's not working. I don't see any property in TelemetryClient to pass LogLeve from AppSettings.
public class Log : ILog
{
private static TelemetryClient telemetryClient = new TelemetryClient() { InstrumentationKey = ConfigurationManager.AppSettings["APPINSIGHTS_INSTRUMENTATIONKEY"] };
public void Error(string message, Exception ex = null)
{
telemetryClient.TrackTrace(message, SeverityLevel.Error);
if (ex != null)
telemetryClient.TrackException(ex);
}
public void Info(string message)
{
telemetryClient.TrackTrace(message, SeverityLevel.Information);
}
public void Verbose(string message)
{
telemetryClient.TrackTrace(message, SeverityLevel.Verbose);
}
public void Warning(string message)
{
telemetryClient.TrackTrace(message, SeverityLevel.Warning);
}
}
host.json
{ "logger": { "categoryFilter": { "defaultLevel": "Information" }}
Function
public static HttpResponseMessage Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req,
ExecutionContext context, [Inject] ILog log)
{
log.Verbose("verbose log");
log.Info("info log");
log.Error("error log");
log.Warning("warning log");
return req.CreateResponse(HttpStatusCode.OK, "Okay");
}
ApplicationInsight
I don't want verbose log in Prod env.
Related
I have a simple function app that uses MediatR pattern and the base function looks like this:
public class ShareEnvelopesFunction
{
private readonly IMediator _mediator;
public ShareEnvelopesFunction(IMediator mediator)
{
_mediator = mediator;
}
[FunctionName(nameof(ShareEnvelopesFunction))]
public async Task Run([TimerTrigger("%ScheduleShareEnvelopesSetting%")]TimerInfo timer, ILogger log)
{
log.LogInformation("Starting Share Envelopes function {0}", timer);
var result = await _mediator.Send(new ShareEnvelopesCommandRequest { log = log });
}
}
As you can see ShareEnvelopesCommandRequest is a class that has only one property which is the ILogger
public class ShareEnvelopesCommandRequest : IRequest<ShareEnvelopesCommandResponse>
{
public ILogger log { get; set; }
}
Now in my command handler, if I use request.log.LogInformation, it logs messages to my console. A sample command handler code looks like this:
public class ShareEnvelopesCommandHandler : IRequestHandler<ShareEnvelopesCommandRequest, ShareEnvelopesCommandResponse>
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDocuSignApiService _docuSignService;
public ShareEnvelopesCommandHandler(IUnitOfWork unitOfWork
, IDocuSignApiService docuSignService
)
{
_unitOfWork = unitOfWork;
_docuSignService = docuSignService;
}
public async Task<ShareEnvelopesCommandResponse> Handle(ShareEnvelopesCommandRequest request, CancellationToken cancellationToken)
{
request.log.LogInformation("Starting to share envelopes");
await _docuSignService.ShareEnvelopes(fromGroupUser.UserId, deltaUsers);
return new ShareEnvelopesCommandResponse() {isSuccess=true };
}
Now the real issue here is that, if you see the above code, I am injecting a docusign service and inside this service, I need to log certain information into the console. My sample docusign service looks like this:
public class DocuSignApiService : IDocuSignApiService
{
public IGroupsApi _groupsApi { get; set; }
public IAccountsApi _accountsApi { get; set; }
public DocuSignApiService(IGroupsApi groupsApi, IAccountsApi accountsApi)
{
_groupsApi = groupsApi;
_accountsApi = accountsApi;
}
public async Task ShareEnvelopes(string fromUserId, List<string> toUsersList)
{
//_logger.LogInformation("This is a test");
}
}
Now I need to be able to log any information from this service to the console. Now I can pass the ShareEnvelopesCommandRequest request to this service but I don't think that would be very efficient. So here is what I have tried:
I injected ILogger into the service:
public class DocuSignApiService : IDocuSignApiService
{
public IGroupsApi _groupsApi { get; set; }
public IAccountsApi _accountsApi { get; set; }
private readonly ILogger _logger;
public DocuSignApiService(IGroupsApi groupsApi, IAccountsApi accountsApi, ILogger<ShareEnvelopesCommandRequest> logger )
{
_groupsApi = groupsApi;
_accountsApi = accountsApi;
_logger = logger;
}
public async Task ShareEnvelopes(string fromUserId, List<string> toUsersList)
{
_logger.LogInformation("This is a test");
}
}
The configuration on DI in my startup class looked like this:
services.AddScoped<IDocuSignApiService>(_ => new DocuSignApiService(docuSignConfig,
_.GetService<IGroupsApi>(),
_.GetService<IAccountsApi>(),
_.GetService<ILogger<ShareEnvelopesCommandRequest>>()
)
);
However, doing that didn't log information into the azure function app console. Are there an ideas on how I can go about logging messages from a service into the console? Thanks in advance.
Maybe you have to setup log level in host.json like this:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
},
"logLevel": {
"YourNamespace.DocuSignApiService": "Information",
"YourNamespace.ShareEnvelopesFunction": "Information",
}
}
}
I'm guessing you get the ILogger instance in your Run method directly from azure, whenever you run the function.
_.GetService<ILogger<ShareEnvelopesCommandRequest>>()
That above probably gets another instance unrelated to the one azure provides you and maybe that's why no logs happen.
If you're going to use DI, you gotta do it from the start of the Run method of your function class (ShareEnvelopesFunction).
public class ShareEnvelopesFunction
{
private readonly IMediator _mediator;
private readonly ServiceCollection _serviceCollection;
public ShareEnvelopesFunction(IMediator mediator, ServiceCollection serviceCollection)
{
_serviceCollection = serviceCollection;//inject this from your startup
_mediator = mediator;
}
[FunctionName(nameof(ShareEnvelopesFunction))]
public async Task Run([TimerTrigger("%ScheduleShareEnvelopesSetting%")] TimerInfo timer, ILogger log)
{
_serviceCollection.AddSingleton(log);//this should ensure that all log instances injected into your services are references to the one azure provided, or something like that
_serviceCollection.AddScoped<IDocuSignApiService,DocuSignApiService>();//now that your servicecollection knows what ILogger to inject, you can also inject your service normally, not sure if this needs to be done here tho, leaving it in your startup should be fine
log.LogInformation("Starting Share Envelopes function {0}", timer);
var result = await _mediator.Send(new ShareEnvelopesCommandRequest { log = log });
}
}
I've done this in a recent AWS Lambda project, their ILambdaContext also serve as logger. I was having the same issue as you, no logs in DI dependent services.
If that doesn't do the trick, try rebuilding your ServiceProvider before Mediator does it's thing.
using (IServiceProvider serviceProvider = _serviceCollection.BuildServiceProvider())
{
log.LogInformation("Starting Share Envelopes function {0}", timer);
_mediator = serviceProvider.GetService<IMediator>();
var result = await _mediator.Send(new ShareEnvelopesCommandRequest { log = log });
}
I'm trying to write the request response traces to a file in an EWS Managed API client application. The problem is, at a time there are multiple requests/response (multiple emails) traces logged in a file, due to which race condition occurs resulting in "The process cannot access the file because it is being used by another process." error.
Adding lock worked , but I wanted check if there's any other better way to handle this scenario
public class TraceListener : ITraceListener
{
private static object leadLock = new object();
private readonly string traceFilePath;
private readonly ILogger _logger;
private readonly string traceFileName;
private readonly bool enableTrace;
public TraceListener(ILogger<TraceListener> logger, IConfiguration configuration)
{
traceFilePath = configuration["TraceFilePath"];
traceFileName = configuration["TraceFileName"];
bool.TryParse(configuration["EnableTrace"], out enableTrace);
_logger = logger;
}
#region ITraceListener Members
public void Trace(string traceType, string traceMessage)
{
if (enableTrace)
CreateTextFile(traceType, traceMessage.ToString());
}
#endregion
public void CreateTextFile(string fileName, string traceContent)
{
try
{
lock (leadLock)
{
Directory.CreateDirectory(traceFilePath);
File.AppendAllText(Path.Combine(traceFilePath, fileName + ".txt"), traceContent);
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while writing traces to the file");
}
finally
{
_logger.LogInformation("TraceListner ended");
}
}
}
I am trying to write a wrapper to abstract away logging frameworks. As part of the effort, I am trying to instantiate logging services from string names. When I try the below code to create an instance of one of the services it fails with the stated error. At the same time another service implementing the same interface and with a similar constructor works.
ILoggerService.cs
public interface ILoggerService
{
void Log(string logLevel, string state, string eventId);
}
NLogService
public class NLogService : ILoggerService
{
private NLog.Logger _logger;
private NLogNotifications _notifications;
public NLogService(Func<LoggerConfiguration> notifiers)
{
_notifications = new NLogNotifications(notifiers);
_logger = NLog.LogManager.GetCurrentClassLogger();
}
public void Log(string logLevel, string logMessage, string category)
{
_logger = NLog.LogManager.GetLogger(category);
_logger.Log(GetLogLevel(logLevel), logMessage, category);
}
private NLog.LogLevel GetLogLevel(string levelName)
{
if (levelName == "Critical") levelName = "Fatal";
return NLog.LogLevel.FromString(levelName);
}
}
Log4NetService
public class Log4NetService : ILoggerService
{
private log4net.ILog _logger;
private Log4NetNotifications _notifications;
public Log4NetService(Func<LoggerConfiguration> config)
{
_logger =
log4net.LogManager.GetLogger(typeof(Log4NetService));
BasicConfigurator.Configure();
_notifications = new Log4NetNotifications(config);
}
void ILoggerService.Log(string logLevel, string logMessage, string category)
{
_logger = log4net.LogManager.GetLogger(category);
_logger.Logger.Log(GetLogEvent(logLevel, logMessage, category));
}
private log4net.Core.LoggingEvent GetLogEvent(string logLevel, string logMessage, string category)
{
return new log4net.Core.LoggingEvent(new log4net.Core.LoggingEventData() {
Level = GetLogLevel(logLevel),
Message = logMessage,
LoggerName = category
});
}
private Level GetLogLevel(string logLevel)
{
return _logger.Logger.Repository.LevelMap[logLevel];
}
}
The code that throws the exception looks like below -->
(_name, _getCurrentConfig, Logger) = (name, getCurrentConfig,
(ILoggerService?)Activator.CreateInstance(Type.GetType(lname)?.Assembly.FullName ?? "",
Type.GetType(lname)?.FullName ?? "", new [] { getCurrentConfig })?.Unwrap());
I'm working on a fun project for myself to learn blazor, a lot of core concepts and just general practice.
I'm trying to implement logger extension for FakeItEasy to easier test Microsofts ILogger.
Based on https://dev.azure.com/BrideButler/Bride%20Butler%20Open%20Source/_git/FakeItEasy.DotnetCore.LoggerExtensions?path=%2FFakeItEasy.DotnetCore.LoggerExtensions%2FLoggerExtensions.cs but now trying to get it to work for .NET 5.
Now that FormattedLogValues is internal this code doesn't work anymore.
The goal is to have something like this in my test (and not use Moq)
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
or A.CallTo(() => _logger.Log(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly(); It doesn't have to have syntactic sugar but I would like to get it to work with FakeItEasy instead of Moq.
Below is all the involved code. Ofcourse I see some difference in the first line Microsoft.Extensions.Logging.ILogger.Log1[System.Object]vsMicrosoft.Extensions.Logging.ILogger1[Server.Controllers.WeatherForecastController].Log1[Microsoft.Extensions.Logging.FormattedLogValues]` but I've no clue how to "fix" this since I dont fully understand what is going wrong.
My question is obviously, what should I do to fix it, but also curious what part i'm missing.
My error is the following:
Assertion failed for the following call:
Microsoft.Extensions.Logging.ILogger.Log`1[System.Object]
(
logLevel: Information,
eventId: <Ignored>,
state: <e => e.ToString().Contains(value(Server.Tests.Extensions.LoggerExtensions+<>c__DisplayClass2_0`1[Server.Controllers.WeatherForecastController]).message)>,
exception: <Ignored>,
formatter: <Ignored>
)
Expected to find it once exactly but didn't find it among the calls:
1: Microsoft.Extensions.Logging.ILogger`1[Server.Controllers.WeatherForecastController].Log`1[Microsoft.Extensions.Logging.FormattedLogValues]
(
logLevel: Information,
eventId: 0,
state: [[{OriginalFormat}, Get Weather Called]],
exception: NULL,
formatter: System.Func`3[Microsoft.Extensions.Logging.FormattedLogValues,System.Exception,System.String]
)
Tests:
(using xUnit, FakeItEasy, Fixture)
LoggerExtensions
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level,
string message)
{
return A.CallTo(() => logger.Log(
level,
A<EventId>._,
A<object>.That.Matches(e => e.ToString().Contains(message)),
A<Exception>._,
A<Func<object, Exception, string>>._)
);
}
}
TestConstructor:
private readonly IFixture _fixture;
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastControllerTests()
{
_fixture = new Fixture().Customize(new AutoFakeItEasyCustomization());
_logger = _fixture.Freeze<Fake<ILogger<WeatherForecastController>>>().FakedObject;
}
Test:
[Fact]
public void WeatherForecast_Get_Should_Log()
{
// Arrange
var weatherController = new WeatherForecastController(_logger);
// Act
weatherController.Get();
// Assert
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
}
Controller:
public class WeatherForecastController : ControllerBase
{
private readonly ILogger _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// Left out other code then logging
_logger.Log(LogLevel.Information, "Get Weather Called");
}
}
Update:
Options found
Only test if logger was called with correct log level:
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger).Where(call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == level);
}
Switch to Moq:
https://stackoverflow.com/a/59182490/1112413
https://stackoverflow.com/a/56728528/1112413
After reading https://stackoverflow.com/users/4728685/pavel-anikhouski comment on the post. I "got" what was wrong. Went to countless topics and made a solution that "works" tho probably could use some better coding standards. Hope it helps anyone else!
Edit: Some explaining.
Found this github comment that is the base of the solution. Then found this github pr where they mentioned to change Microsoft.Extensions.Logging.FormattedLogValues to IReadOnlyList<KeyValuePair<string, object>>
This can be extracted from GetArgument(2). (Source: https://stackoverflow.com/a/63701968/1112413)
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger)
.Where(call => call.Method.Name == "Log"
&& call.GetArgument<LogLevel>(0) == level
&& CheckLogMessages(call.GetArgument<IReadOnlyList<KeyValuePair<string, object>>>(2), message));
}
private static bool CheckLogMessages(IReadOnlyList<KeyValuePair<string, object>> readOnlyLists, string message)
{
foreach(var kvp in readOnlyLists)
{
if (kvp.Value.ToString().Contains(message))
{
return true;
}
}
return false;
}
}
Based on the same comments of Github
I ended up with a solution like that:
public class MyClassWithLoggerTests
{
private readonly MyClassWithLogger _sut;
private readonly ILogger<MyClassWithLogger> _logger;
public MyClassWithLoggerTests()
{
this._logger = A.Fake<ILogger<MyClassWithLogger>>();
this._sut = new MyClassWithLogger(this._logger);
}
[Fact]
public ATestMethodTitle()
{
this._sut.MethodToTest();
A.CallTo(_logger).Where(
call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == LogLevel.Information) // Modify with your type of log
.MustHaveHappened(1, Times.Exactly); // Modify with the number of times log is called
}
}
First of all a bit of background.
I am using .Net Framework 4.6.1, Microsoft.AspNet.WebApi 5.2.4 in Visual Studio 2017 Community.
My ApiController's implement endpoints which throw intended Exceptions for example if certain requirements are not met. I added global ExceptionFilterAttribute and ExceptionHandler to handle those Exceptions and to return a proper response.
The Exceptions are of a type which are inherited of System.Exception.
This is only working occasionally as intended. Every second or third or sometimes fifth (no real pattern) request the api server returns no response at all e.g. for example Postman says: "Could not get any response".
To test this I used the same endpoint with the same input.
Here are a few things I did to get a better idea of the problem:
I added exception logging to Global.asax (to catch First Chance Exceptions)
I subscribed to Global.asax Application_Error Event
I looked at the IIS logs
None of those got my closer to the issue. The exception was caught and logged in Global.asax like expected but no additional error or exception which could give me more info to my problem.
Here is my code:
I simplified the ApiController's function and removed the business logic.
[Route("test")]
[HttpGet]
public IHttpActionResult GetTest()
{
throw new ObjectAlreadyExistsException("test");
return ResponseFactory.CreateOkResponse(null);
}
public class ExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is ObjectAlreadyExistsException)
{
context.Response = ResponseFactory.CreateErrorResponseMessage(context.Exception.Message, new Error("OBJECT_ALREADY_EXISTS_ERROR", context.Exception.Message));
}
else if (context.Exception is ObjectNotFoundException)
{
context.Response = ResponseFactory.CreateErrorResponseMessage(context.Exception.Message, new Error("OBJECT_NOT_FOUND_ERROR", context.Exception.Message));
}
base.OnException(context);
}
}
public class GlobalExceptionHandler : ExceptionHandler
{
private static readonly ILogger Log = new LoggerConfiguration()
.WriteTo.File(new CompactJsonFormatter(), Path.Combine(#Properties.Settings.Default.LOG_DIRECTORY, #"error.json"), rollOnFileSizeLimit: true, retainedFileCountLimit: 5, shared: true)
.Enrich.WithWebApiControllerName()
.Enrich.WithWebApiActionName()
.Enrich.WithWebApiRouteTemplate()
.Enrich.WithWebApiRouteData()
.Enrich.With(new AuthTokenEnricher())
.CreateLogger();
public override void Handle(ExceptionHandlerContext context)
{
if (context != null && context.Exception != null)
{
Log.Error("Unexpected Internal Server Error {Exception}", context.Exception);
}
context.Result = ResponseFactory.CreateErrorResponse(HttpStatusCode.InternalServerError, "Unexpected Internal Server Error", new Error("INTERNAL_SERVER_ERROR", "This request failed because of an unexpected server error."));
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//Exception filters
config.Filters.Add(new ExceptionFilter());
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
// Web API routes
config.MapHttpAttributeRoutes();
}
}
public class ObjectAlreadyExistsException : Exception
{
public ObjectAlreadyExistsException(string message) : base(message)
{
}
public ObjectAlreadyExistsException(string message, Exception inner) : base(message, inner)
{
}
}
For now I put a workaround in place which looks like this:
[Route("test")]
[HttpGet]
public IHttpActionResult GetTest()
{
try
{
throw new ObjectAlreadyExistsException("test");
}
catch (Exception ex)
{
return CustomExceptionHandler.Handle(ex);
}
}
public class CustomExceptionHandler
{
private static readonly ILogger Log = new LoggerConfiguration()
.WriteTo.File(new CompactJsonFormatter(), Path.Combine(#Properties.Settings.Default.LOG_DIRECTORY, #"error.json"), rollOnFileSizeLimit: true, retainedFileCountLimit: 5, shared: true)
.Enrich.WithWebApiControllerName()
.Enrich.WithWebApiActionName()
.Enrich.WithWebApiRouteTemplate()
.Enrich.WithWebApiRouteData()
.Enrich.With(new AuthTokenEnricher())
.CreateLogger();
public static IHttpActionResult Handle(Exception ex)
{
IHttpActionResult result = null;
if (ex != null)
{
if (ex is ObjectAlreadyExistsException)
{
result = ResponseFactory.CreateErrorResponse(ex.Message, new Error("OBJECT_ALREADY_EXISTS_ERROR", ex.Message));
}
else if (ex is ObjectNotFoundException)
{
result = ResponseFactory.CreateErrorResponse(ex.Message, new Error("OBJECT_NOT_FOUND_ERROR", ex.Message));
}
}
if (result == null)
{
if (ex != null)
{
Log.Error("Unexpected Internal Server Error {Exception}", ex);
}
result = ResponseFactory.CreateErrorResponse(HttpStatusCode.InternalServerError, "Unexpected Internal Server Error", new Error("INTERNAL_SERVER_ERROR", "This request failed because of an unexpected server error."));
}
return result;
}
}
I would appreciate any ideas how to debug this or any suggestions to fix it.
Can you try inheriting from IHttpActionResult and use it as returning the exception from your GlobalExceptionHandler
private class ErrorMessageResult : IHttpActionResult
{
private readonly HttpResponseMessage _httpResponseMessage;
private HttpRequestMessage _request;
public ErrorMessageResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
{
_request = request;
_httpResponseMessage = httpResponseMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_httpResponseMessage);
}
}
and call it like,
public override void Handle(ExceptionHandlerContext context)
{
var result = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("Internal Server Error Occurred"),
ReasonPhrase = "Exception"
};
context.Result = new ErrorMessageResult(context.Request, result);
}
From GlobalExceptionHandler : ExceptionHandler