Searching I've not found the answer I'm looking for. I will only put code snippets here to help ask my question, supplying ALL of the code would not be executable without the entire system.
I'm using the built-in logging in C# to log to Windows Event Viewer and/or the Console. I also wanted to write to a file but not third-party logging, so I wrote my own simple logger that logs the same data to files and works.
My appsettings.json as a second for some configuration parameters like a working folder. I also have a folder path for the logger. What I would like to be able to do is use the same holder path from the setting and not have 2.
appsettings.json
{
"AppSettings": {
"WorkingFolderPath": "C:\\mypath\\",
"HeartbeatIntervalMinutes": 0, // This is seconds when in debug mode.
"FileRetryDelayMinutes": 1,
"PingTimeoutMilliseconds": 200
},
"Logging": {
"LogLevel": {
"Default": "Error",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
},
"Debug": {
"LogLevel": {
"Default": "Debug"
}
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Default": "Debug"
}
},
"EventLog": {
"LogLevel": {
"Default": "Warning"
}
},
"FileLog": {
"Options": {
"FolderPath": "C:\\mypath\\",
"RetentionDays": 5
},
"LogLevel": {
"Default": "Debug"
}
}
}
}
As you see in this file I have a "WorkingFolderPath" setting I'd like to use that as my path in my "FileLog" logger and not have to specify a path there as well.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureLogging((context, logging) =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
logging.AddEventLog(new EventLogSettings()
{
SourceName = "FreshIQAppMessagingService"
});
logging.AddFileLogger(options =>
{
context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options);
});
})
.ConfigureAppConfiguration((hostContext, config) =>
{
config
.SetBasePath(ApplicationPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
})
.ConfigureServices((hostContext, services) =>
{
services.Configure<Configuration>(hostContext.Configuration.GetSection("AppSettings"));
services.AddHostedService<Service1>();
services.AddHostedService<Service2>();
services.AddHostedService<Service3>();
services.AddHostedService<Service4>();
});
Here it's getting the options and passing them in. What I've not been able to figure out is how to change my classes so that I can send in the "WorkingFolderPath" from the top of the appsettings instead of the one with the options with the FileLog logger.
logging.AddFileLogger(options =>
{
context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options);
});
FileLoggerExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace FreshIQAppMessaging.Logging
{
public static class FileLoggerExtensions
{
public static ILoggingBuilder AddFileLogger(this ILoggingBuilder loggingBuilder, Action<FileLoggerOptions> configure)
{
loggingBuilder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
loggingBuilder.Services.Configure(configure);
return loggingBuilder;
}
}
}
FileLoggerProvider.cs
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.IO;
namespace FreshIQAppMessaging.Logging
{
[ProviderAlias("FileLog")]
public class FileLoggerProvider : ILoggerProvider
{
public readonly FileLoggerOptions Options;
public FileLoggerProvider(IOptions<FileLoggerOptions> options)
{
Options = options.Value;
if (!Directory.Exists(Options.FolderPath))
{
Directory.CreateDirectory(Options.FolderPath);
}
}
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(this);
}
public void Dispose() { }
}
}
FileLogger.cs
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.IO;
namespace FreshIQAppMessaging.Logging
{
public class FileLogger : ILogger
{
protected readonly FileLoggerProvider _fileLoggerProvider;
public FileLogger(FileLoggerProvider fileLoggerProvider)
{
_fileLoggerProvider = fileLoggerProvider;
}
public IDisposable? BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
// Clean up old log files
var logFiles = Directory.GetFiles(_fileLoggerProvider.Options.FolderPath, "*-MyApp.log");
foreach (var logFilePath in logFiles)
{
var logFileName = new FileInfo(logFilePath).Name;
if (DateTime.TryParseExact(logFileName.Substring(0, 8), "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var logFileDate))
{
if (logFileDate.AddDays(_fileLoggerProvider.Options.RetentionDays) < DateTime.Now)
{
File.Delete(logFilePath);
}
}
}
var fullFilePath = $"{_fileLoggerProvider.Options.FolderPath}\\{DateTime.Now.ToString("yyyyMMdd")}-MyApp.log";
var logRecord = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} [{logLevel.ToString()}] {formatter(state, exception)} {(exception != null ? exception.StackTrace : "")}";
using (var streamWriter = new StreamWriter(fullFilePath, true))
{
streamWriter.WriteLine(logRecord);
}
}
}
}
FileLoggerOptions.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace FreshIQAppMessaging.Logging
{
public class FileLoggerOptions
{
public virtual string? FolderPath { get; set; }
public virtual int RetentionDays { get; set; } = 5;
}
}
You typically don't want to include custom options for your logger inside the Logging section. Most people do something like this instead:
{
"AppSettings": {
"WorkingFolderPath": "C:\\mypath\\",
...
},
"FileLog": {
"FolderPath": "C:\\mypath\\",
"RetentionDays": 5
},
"Logging": {
...
"FileLog": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
You didn't share your options class but I assume you have a property on it named FolderPath. You could then configure your file logger like this:
logging.Services.Configure<FileLoggerOptions>(builder.Configuration.GetSection("FileLog"));
logging.AddFileLogger(options =>
{
options.FolderPath = builder.Configuration["AppSettings:WorkingFolderPath"];
});
You can now remove the FolderPath property from the FileLog object in the appsettings.json file since that value will be set using this line when adding the file logger:
options.FolderPath = builder.Configuration["AppSettings:WorkingFolderPath"];
The options would then look like this:
{
"AppSettings": {
"WorkingFolderPath": "C:\\mypath\\",
...
},
"FileLog": {
"RetentionDays": 5
},
"Logging": {
...
"FileLog": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
I've written a similar logging provider in the Elmah.Io.Extensions.Logging repository and I copied some of the code from there. I tried mapping it to your original code in the question, but there might be something that doesn't match exactly. I hope that you still see the intent behind what I'm trying. You can look inside this repository for inspiration.
Related
I am wanting to return the logs that contain "level": "error" from elasticsearch in a asp.net core web api application using NEST. I looked into the search_after api Search After API and looked into another resource for Paginate Search After. On the Kibana CLI, I wrote the following:
GET elastic-search-app-log*/_search
{
"size": 3000,
"query": {
"match": {
"level": "Error"
}
},
"search_after": [3000],
"sort":[
{"#timestamp": "asc"}
]
}
I just set the size to something random along and for it just to search after each 3000 indice.
So on the .net side of it, I attempted to translate it as so:
ESFieldsController
private readonly IElasticClient _elasticClient;
public ESFieldsController(IElasticClient elasticClient)
{
_elasticClient = elasticClient;
}
[HttpGet]
public async Task<ESFields> Get()
{
var response = await _elasticClient.SearchAsync<ESFields>(s => s
.Index("elastic-search-app-logs*")
.Size(3000)
.Query(q => q.Match(m => m.Field(f => f.Level == "error")))
.SearchAfter(3000)
.Sort(srt => srt
.Ascending(p => p.TimeStamp)));
Console.WriteLine(response);
return response?.Documents?.FirstOrDefault();
}
ESFields
namespace ESPractice.Models
{
public class ESFields
{
public String Level { get; set; }
public DateTime TimeStamp { get; set; }
}
}
However, when I run the application and swagger comes up, I try to execute the get cmd to see if it works but it does not return the logs that contain "level": "error". Is there something I am doing incorrectly with the translation?
Additional info:
startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ESPractice", Version = "v1" });
});
// create a new node instance
var node = new Uri("http://localhost:9200");
// settings instance for the node
var settings = new ConnectionSettings(node);
services.AddSingleton<IElasticClient>(new ElasticClient(settings));
}
Everything below is from a separate application where I am writing the logs to elasticsearch:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog((context, configuration) =>
{
configuration.Enrich.FromLogContext()
.Enrich.WithMachineName()
.WriteTo.Console()
.WriteTo.Elasticsearch(
new ElasticsearchSinkOptions(new Uri(context.Configuration["ElasticConfiguration:Uri"]))
{
IndexFormat = $"{context.Configuration["ApplicationName"]}-logs-{context.HostingEnvironment.EnvironmentName?.ToLower().Replace(".", "-")}-{DateTime.UtcNow:yyyy-MM}",
AutoRegisterTemplate = true,
})
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
.ReadFrom.Configuration(context.Configuration);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
appsettings.json
{
"ApplicationName": "elastic-search-app",
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
}
},
"ElasticConfiguration": {
"Uri": "http://localhost:9200"
},
"AllowedHosts": "*"
}
This is my appsetting.json file Reading AppSettings.json file please understand my Solution Structure
ElasticCoreWebApi //.net Core Web Application 3.1
-> AppSettings.json
ElasticCoreWebApi.Utility
->CommonObject.cs //.net Console Application
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"elasticsearch": {
"AcqIndex": "MyIndex",
"AcqDataUrl": "MyUrl",
"AcqNodeId": "MyUser",
"AcqNodePwd": "MyPassword"
},
"MySettings": {
"Parameters": {
"LogsPath": "E:\\Logs\\",
"LogDir": "LogAsad",
"ErrorLogDir": "ErrorLog",
"ServiceLogDir": "ServiceLog",
"UserName": "username123",
"Password": "password123",
"TripleDesKey": "MyDESKey",
"LogType": "FILE",
"LogConnectionString": "LogConnectionString",
"LogTable": "LOGGER",
"LogDirectory": "C:\\Logs\\asad.NiSolution.pk\\",
"MaxNoOfLinesInLogFile": "50000",
"LogLevel": "Error,SystemTrace,FunctionTrace,Information",
"ERPName": "erp123",
"ERPPassword": "erp#321!"
}
}
}
This is my CommonObject.cs Class file in ElasticCoreWebApi.Utility what i am trying to do is Reading AppSetting.json MySettings all things just like what i was doing from Asp.net WebForm WebConfig file which was written in XML in app-setting Tag
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
//using WSUtil = WS.Core.Util;
namespace ElasticCoreWebApi.Utility
{
public class CommonObjects
{
#region Configuration
public static string GetCongifValue(string ConfigKey)
{
return ConfigurationManager.AppSettings[ConfigKey].ToString();
}
public static string GetConnectionString()
{
//return ConfigurationManager.ConnectionStrings[GenericConstants.ConnectionStringKey].ConnectionString;
return ((new WSUtil.TripleDES()).Decrypt(ConfigurationManager.ConnectionStrings[GenericConstants.ConnectionStringKey].ConnectionString));
}
#endregion
}
}
Hopefully this can help.
There is a nuget package called "Microsoft.Extensions.Configuration.Json".
I believe that code is part of asp.net, but it can be added as a nuget package to a .net core project.
Some sample code:
// using ...
using Microsoft.Extensions.Configuration;
namespace ElasticCoreWebApi.Utility
{
public class Settings
{
private IConfigurationRoot _config;
public Settings()
{
// Ensure that files exists
// You may want to add some default settings in case it is missing
_config = new ConfigurationBuilder().AddJsonFile(/*File path goes here*/).Build();
}
public T Get<T>(string section, string key)
{
try
{
return (T)Convert.ChangeType(_config.GetSection(section)[key], typeof(T));
}
catch (InvalidCastException e)
{
_logger.Error(e.Message);
return default;
}
}
}
}
Some adjustments might be needed to cover things like child dictionary but it might be a place to start.
I am making .Net core application and use Serilog for logging. Now I want to use Serilog to write a log for each clients who already logged in.
I expected the application gonna have seperate logger
Global logger: write server log
Client loggers: each client gonna have their own logger to write log file.
From what I tried to do the application do generate the txt file. However, it doesn't write anything to it.
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseIISIntegration()
.UseSerilog((hostingcontext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingcontext.Configuration))
.UseStartup<Startup>();
});
}
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<LoggerManager>();
}
...MORE CODE...
}
LoggerManager.cs
public class LoggerManager
{
public static Dictionary<string, ILogger> loggerDict = new Dictionary<string, ILogger>();
public void CreateUserLogger(string username, EnumType userType)
{
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.Async(a => a.File(#$"Log/{userType}/{username}.txt",
buffered:true,
rollingInterval:RollingInterval.Day,
retainedFileCountLimit:90))
.CreateLogger();
loggerDict.TryAdd(cheID, logger);
}
public void WriteInfoLog (string username, string message)
{
loggerDict.TryGetValue(username, out ILogger logger);
logger.Information(message);
}
}
HomeController.cs
class HomeController {
private readonly LoggerManager _loggerManager;
public HomeController(LoggerManager loggerManager)
{
_loggerManager = loggerManager;
}
public IActionResult Index() {
_loggerManager.WriteInfoLog(authenticatedUser.CHEID, "HELLO HOW ARE U TODAY");
return View();
}
}
appsettings.json
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "Async",
"Args": {
"configure": [
{
"Name": "File",
"Args": {
"path": "Logs/Server/serverlog.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 7,
"buffered": true
}
}
]
}
}
]
}
I randomly found the "sinks-map". This sinks definitely satisfy my requirement.
https://github.com/serilog/serilog-sinks-map
I have written my C# logic in a console application using the .net core and sdk on visual studio code. I'm trying to add a config file to configure certain parameters, however, once I create the appsettings.json file, I'm unable to access it in the code. Need help to access the values from config file.
using System;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace testapp
{
class Program
{
static void Main(string[] args)
{
var appSettings = new Config();
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables() //This line doesnt compile
.AddJsonFile("appsettings.json", true, true)
.Build();
config.Bind(appSettings); //This line doesnt compile
Console.WriteLine("Hello World!");
Console.WriteLine($" Hello {appSettings.name} !");
}
}
public class Config
{
public string name{ get; set; }
}
}
HomeController.cs
public class HomeController : Controller {
private readonly IConfiguration _config;
public HomeController(IConfiguration config) { _config = config; }
public IActionResult Index() {
Console.WriteLine(_config.GetSection("AppSettings:Token").Value);
return View();
}
}
appsettings.json
{
"AppSettings": {
"Token": "T21pZC1NaXJ6YWVpWhithOutStar*"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
Try this -
using System;
using Microsoft.Extensions.Configuration;
namespace testapp
{
class Program
{
static void Main(string[] args)
{
var appSettings = new Config();
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json", true, true)
.Build();
config.Bind(appSettings);
Console.WriteLine($" Hello {appSettings.name} !");
}
}
public class Config
{
public string name{ get; set; }
}
}
appsettings.json
{
"name": "appName"
}
Think I have a problem with the startup.cs as I do not get any values from my <IOption> config
So.. We have our appsettings.json
"Config": {
"ApplicationName": "some name",
"ConnectionString": "someconstring",
"Version": "1.0.0"
},
Here we have our model
public class Config
{
public string ApplicationName { get; set; }
public string ConnectionString { get; set; }
public string Version { get; set; }
}
The startup.cs
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public static IConfiguration Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Add functionality to inject IOptions<T>
services.AddOptions();
// Add our Config object so it can be injected
services.Configure<Config>(Configuration);
}
And then in our controller I try to load those data but unfortunately they remain empty.
private IOptions<Config> config;
public CompaniesController(IOptions<Config> config)
{
this.config = config;
}
I've tried to change the startup.cs with something like
services.Configure<Config>(options =>
{
options.ConnectionString = Configuration.GetSection("Config:ConnectionString").Value;
});
but that doesn't seems to work.
Resources I've been using:
https://dzone.com/articles/dynamic-connection-string-in-net-core
https://stackoverflow.com/questions/31453495/how-to-read-appsettings-values-from-json-file-in-asp-net-core
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.2
but Obviously I am missing a crucial point here.
edit: I am using ASP.Net Core 2.0
edit2:
the Program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
The entire Appsettings.json file
{
"Config": {
"ApplicationName": "somename",
"ConnectionString": "someconstring",
"Version": "1.0.0"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
edit 3:
In my front-end application I import the API like this.
services.Configure<Config>(Configuration);
This line doesn't achieve the desired result because the JSON properties you're looking for are nested under a Config property in your appsettings.json file. To load these values as intended, use GetSection to grab the Config section and pass that into the Configure<TOptions> method:
services.Configure<Config>(Configuration.GetSection("Config"));
services.Configure<Config>(options =>
{
options.ConnectionString = Configuration.GetValue<string>("Config:ConnectionString");
options.ApplicationName = "test";
});
If you want to configure your options more granuarly.