using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Formatting.Compact; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Server; using ZB.MOM.WW.OtOpcUa.Server.OpcUa; using ZB.MOM.WW.OtOpcUa.Server.Security; var builder = Host.CreateApplicationBuilder(args); // Per Phase 6.1 Stream C.3: SIEMs (Splunk, Datadog) ingest the JSON file without a // regex parser. Plain-text rolling file stays on by default for human readability; // JSON file is opt-in via appsetting `Serilog:WriteJson = true`. var writeJson = builder.Configuration.GetValue("Serilog:WriteJson"); var loggerBuilder = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.File("logs/otopcua-.log", rollingInterval: RollingInterval.Day); if (writeJson) { loggerBuilder = loggerBuilder.WriteTo.File( new CompactJsonFormatter(), "logs/otopcua-.json.log", rollingInterval: RollingInterval.Day); } Log.Logger = loggerBuilder.CreateLogger(); builder.Services.AddSerilog(); builder.Services.AddWindowsService(o => o.ServiceName = "OtOpcUa"); var nodeSection = builder.Configuration.GetSection(NodeOptions.SectionName); var options = new NodeOptions { NodeId = nodeSection.GetValue("NodeId") ?? throw new InvalidOperationException("Node:NodeId not configured"), ClusterId = nodeSection.GetValue("ClusterId") ?? throw new InvalidOperationException("Node:ClusterId not configured"), ConfigDbConnectionString = nodeSection.GetValue("ConfigDbConnectionString") ?? throw new InvalidOperationException("Node:ConfigDbConnectionString not configured"), LocalCachePath = nodeSection.GetValue("LocalCachePath") ?? "config_cache.db", }; var opcUaSection = builder.Configuration.GetSection(OpcUaServerOptions.SectionName); var ldapSection = opcUaSection.GetSection("Ldap"); var ldapOptions = new LdapOptions { Enabled = ldapSection.GetValue("Enabled") ?? false, Server = ldapSection.GetValue("Server") ?? "localhost", Port = ldapSection.GetValue("Port") ?? 3893, UseTls = ldapSection.GetValue("UseTls") ?? false, AllowInsecureLdap = ldapSection.GetValue("AllowInsecureLdap") ?? true, SearchBase = ldapSection.GetValue("SearchBase") ?? "dc=lmxopcua,dc=local", ServiceAccountDn = ldapSection.GetValue("ServiceAccountDn") ?? string.Empty, ServiceAccountPassword = ldapSection.GetValue("ServiceAccountPassword") ?? string.Empty, GroupToRole = ldapSection.GetSection("GroupToRole").Get>() ?? new(StringComparer.OrdinalIgnoreCase), }; var opcUaOptions = new OpcUaServerOptions { EndpointUrl = opcUaSection.GetValue("EndpointUrl") ?? "opc.tcp://0.0.0.0:4840/OtOpcUa", ApplicationName = opcUaSection.GetValue("ApplicationName") ?? "OtOpcUa Server", ApplicationUri = opcUaSection.GetValue("ApplicationUri") ?? "urn:OtOpcUa:Server", PkiStoreRoot = opcUaSection.GetValue("PkiStoreRoot") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OtOpcUa", "pki"), AutoAcceptUntrustedClientCertificates = opcUaSection.GetValue("AutoAcceptUntrustedClientCertificates") ?? true, SecurityProfile = Enum.TryParse(opcUaSection.GetValue("SecurityProfile"), true, out var p) ? p : OpcUaSecurityProfile.None, Ldap = ldapOptions, }; builder.Services.AddSingleton(options); builder.Services.AddSingleton(opcUaOptions); builder.Services.AddSingleton(ldapOptions); builder.Services.AddSingleton(sp => ldapOptions.Enabled ? new LdapUserAuthenticator(ldapOptions, sp.GetRequiredService>()) : new DenyAllUserAuthenticator()); builder.Services.AddSingleton(_ => new LiteDbConfigCache(options.LocalCachePath)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Central-config DB access for the host-status publisher (LMX follow-up #7). Scoped context // so per-heartbeat change-tracking stays isolated; publisher opens one scope per tick. builder.Services.AddDbContext(opt => opt.UseSqlServer(options.ConfigDbConnectionString)); builder.Services.AddHostedService(); var host = builder.Build(); await host.RunAsync();