Setup Robin base files

Setup base files for Robin monitoring service
This commit is contained in:
Alexandru-Victor Macocian
2025-11-27 17:37:28 +01:00
parent 4d9ba4ba9e
commit 69fd4bfb62
14 changed files with 493 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
[*.cs]
# CA2254: Template should be a static expression
dotnet_diagnostic.CA2254.severity = none
csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:error
csharp_prefer_simple_using_statement = true:error
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = file_scoped:error
csharp_style_prefer_method_group_conversion = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_indent_braces = false
insert_final_newline = true
csharp_space_around_binary_operators = before_and_after
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:error
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_deconstructed_variable_declaration = true:error
csharp_style_unused_value_assignment_preference = discard_variable:error
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_prefer_static_local_function = true:suggestion
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_prefer_parameter_null_checking = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent
csharp_style_prefer_top_level_statements = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = error
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
dotnet_style_prefer_auto_properties = true:error
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion
dotnet_code_quality_unused_parameters = all:error
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
dotnet_style_qualification_for_field = true:error
dotnet_style_qualification_for_property = true:error
dotnet_style_qualification_for_method = true:error
dotnet_style_qualification_for_event = true:error
dotnet_diagnostic.CA1000.severity = error
[*.vb]
dotnet_diagnostic.CA1047.severity = error
+3
View File
@@ -0,0 +1,3 @@
robin/bin/*
robin/obj/*
robin/.env
+24
View File
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "robin", "robin\robin.csproj", "{A085BD72-D58C-10EC-30F2-771502387E48}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A085BD72-D58C-10EC-30F2-771502387E48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A085BD72-D58C-10EC-30F2-771502387E48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A085BD72-D58C-10EC-30F2-771502387E48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A085BD72-D58C-10EC-30F2-771502387E48}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {295E5EEF-E6AD-459F-B2B1-21E624094138}
EndGlobalSection
EndGlobal
+29
View File
@@ -0,0 +1,29 @@
using System.Core.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace Robin.Configuration;
public static class BuilderExtensions
{
public static IHostApplicationBuilder AddEnvironmentVariables(
this IHostApplicationBuilder builder
)
{
DotNetEnv.Env.Load();
builder.ThrowIfNull().Configuration.AddEnvironmentVariables();
return builder;
}
public static IHostApplicationBuilder AddAppsettings(this IHostApplicationBuilder builder)
{
builder
.ThrowIfNull()
.Configuration.AddJsonFile(
path: "appsettings.json",
optional: false,
reloadOnChange: true
);
return builder;
}
}
+17
View File
@@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy csproj and restore as distinct layers
COPY robin/Robin.csproj ./robin/
RUN dotnet restore ./robin/Robin.csproj
# Copy everything else and build
COPY . .
WORKDIR /src/robin
RUN dotnet publish -c Release -o /app
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Robin.dll"]
+44
View File
@@ -0,0 +1,44 @@
using System.Core.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Settings.Configuration;
namespace Robin.Logging;
public static class BuilderExtensions
{
public static IHostApplicationBuilder ClearProviders(this IHostApplicationBuilder builder)
{
builder.ThrowIfNull().Logging.ClearProviders();
return builder;
}
public static IHostApplicationBuilder AddSerilog(this IHostApplicationBuilder builder)
{
builder
.ThrowIfNull()
.Logging.ClearProviders()
.AddSerilog(
new LoggerConfiguration()
.ReadFrom.Configuration(
builder.Configuration,
new ConfigurationReaderOptions(
typeof(ConsoleLoggerConfigurationExtensions).Assembly
)
{
SectionName = "Serilog",
}
)
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}]{NewLine}{Message:lj}{NewLine}{Exception}",
theme: Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme.Colored
)
.CreateLogger()
);
return builder;
}
}
+16
View File
@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Robin.Options;
public static class BuilderExtensions
{
public static IHostApplicationBuilder AddRobinOptions(this IHostApplicationBuilder builder)
{
builder
.Services.AddOptions<MonitorOptions>()
.Bind(builder.Configuration.GetSection("Monitor"));
return builder;
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Robin.Options;
public sealed class MonitorOptions()
{
public required List<string> Paths { get; init; } = [];
}
+21
View File
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Robin.Configuration;
using Robin.Logging;
using Robin.Options;
using Robin.Services;
using Serilog;
var builder = Host.CreateApplicationBuilder(args);
builder
.AddAppsettings()
.AddEnvironmentVariables()
.ClearProviders()
.AddSerilog()
.AddRobinOptions()
.AddRobinServices();
var host = builder.Build();
await host.RunAsync();
+30
View File
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Formatting.Elasticsearch" Version="10.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="SystemExtensions.NetCore" Version="1.6.12" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings*.json" CopyToOutputDirectory="PreserveNewest" />
<Content Include=".env*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
using System.Core.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Robin.Services;
public static class BuilderExtensions
{
public static IHostApplicationBuilder AddRobinServices(this IHostApplicationBuilder builder)
{
builder.ThrowIfNull();
builder.Services.AddHostedService<MonitoringService>();
return builder;
}
}
+127
View File
@@ -0,0 +1,127 @@
using System.Extensions.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Robin.Options;
namespace Robin.Services;
public sealed class MonitoringService(
IOptions<MonitorOptions> options,
ILogger<MonitoringService> logger
) : IHostedLifecycleService
{
private readonly MonitorOptions options = options.Value;
private readonly ILogger<MonitoringService> logger = logger;
private readonly List<FileSystemWatcher> watchers = [];
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) =>
this.MonitorFiles(cancellationToken);
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public async Task MonitorFiles(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug(
"Starting file monitoring service. Parsing provided paths {paths}",
string.Join(", ", this.options.Paths)
);
var expandedPaths = ExpandPaths(this.options.Paths);
scopedLogger.LogInformation(
"Monitoring the following files: {files}",
string.Join(", ", expandedPaths)
);
foreach (var filePath in expandedPaths)
{
var watcher = CreateWatcher(filePath);
if (watcher is null)
{
scopedLogger.LogWarning("Failed to watch file: {file}", filePath);
continue;
}
this.watchers.Add(watcher);
scopedLogger.LogDebug("Watching file: {file}", filePath);
}
}
private FileSystemWatcher? CreateWatcher(string filePath)
{
var scpoedLogger = this.logger.CreateScopedLogger();
var directory = Path.GetDirectoryName(filePath);
var fileName = Path.GetFileName(filePath);
if (directory is null || fileName is null)
{
scpoedLogger.LogWarning("Invalid file path: {filePath}", filePath);
return default;
}
var watcher = new FileSystemWatcher(directory, fileName)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = true,
};
watcher.Changed += this.OnFileChanged;
watcher.Error += this.OnWatcherError;
return watcher;
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("File changed: {file}", e.FullPath);
// TODO:: Read changes and push to Elastic
}
private void OnWatcherError(object sender, ErrorEventArgs e)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogError(e.GetException(), "Watcher error");
}
public void Dispose()
{
foreach (var watcher in this.watchers)
{
watcher.Changed -= this.OnFileChanged;
watcher.Error -= this.OnWatcherError;
watcher.Dispose();
}
this.watchers.Clear();
}
private static List<string> ExpandPaths(List<string>? patterns)
{
if (patterns is null || patterns.Count == 0)
{
return [];
}
var results = new List<string>();
foreach (var pattern in patterns)
{
var directory = Path.GetDirectoryName(pattern);
var filePattern = Path.GetFileName(pattern);
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
{
continue;
}
results.AddRange(Directory.GetFiles(directory, filePattern));
}
return results;
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"Robin.Services": "Debug"
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: robin
env_file:
- .env
restart: unless-stopped