From 0ba597dcbc720a2a1ef57ec6cd2362d272f2a0a7 Mon Sep 17 00:00:00 2001 From: moe Date: Thu, 30 Apr 2026 14:13:46 -0300 Subject: [PATCH 1/2] Move microservice log level context to tooling common Move the ambient microservice log-level AsyncLocal into beamable.tooling.common so AdminRoutes and runtime logging share the same context without introducing a runtime dependency. Scope OpenAPI docs generation to temporarily raise the log threshold to Warning and restore the previous level afterward. Preserve the existing MicroserviceBootstrapper.ContextLogLevel API as an obsolete forwarder for compatibility. --- .../Microservice/AdminRoutes.cs | 32 +++++++++++++------ .../MicroserviceLogLevelContext.cs | 10 ++++++ .../Api/RealmConfig/RealmConfigService.cs | 2 +- .../dbmicroservice/BeamableMicroService.cs | 24 +++++++------- .../MicroserviceBootstrapper.cs | 3 +- .../dbmicroservice/MicroserviceStartupUtil.cs | 6 ++-- 6 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 microservice/beamable.tooling.common/Microservice/MicroserviceLogLevelContext.cs diff --git a/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs b/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs index d110b983c9..238cda7033 100644 --- a/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs +++ b/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs @@ -70,7 +70,7 @@ public string HealthCheck() { return "responsive"; } - + /// /// Generates an OpenAPI/Swagger 3.0 document that describes the available service endpoints. /// @@ -84,19 +84,31 @@ public string HealthCheck() [CustomResponseSerializationAttribute] public string Docs() { - var docs = new ServiceDocGenerator(); - var ctx = GlobalProvider.GetService(); - var doc = docs.Generate(ctx, GlobalProvider); - - if (!string.IsNullOrEmpty(PublicHost)) + // Suppress info-level logs during OpenAPI doc generation to avoid noise + //Wrapping with try/finally to ensure log level is restored even if an exception occurs + var previousLogLevel = MicroserviceLogLevelContext.CurrentLogLevel.Value; + MicroserviceLogLevelContext.CurrentLogLevel.Value = LogLevel.Warning; + + try { - doc.Servers.Add(new OpenApiServer { Url = PublicHost }); - } + var docs = new ServiceDocGenerator(); + var ctx = GlobalProvider.GetService(); + var doc = docs.Generate(ctx, GlobalProvider); + + if (!string.IsNullOrEmpty(PublicHost)) + { + doc.Servers.Add(new OpenApiServer { Url = PublicHost }); + } - var outputString = doc.Serialize(OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); + var outputString = doc.Serialize(OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); - return outputString; + return outputString; + } + finally + { + MicroserviceLogLevelContext.CurrentLogLevel.Value = previousLogLevel; + } } /// diff --git a/microservice/beamable.tooling.common/Microservice/MicroserviceLogLevelContext.cs b/microservice/beamable.tooling.common/Microservice/MicroserviceLogLevelContext.cs new file mode 100644 index 0000000000..92b77f6a66 --- /dev/null +++ b/microservice/beamable.tooling.common/Microservice/MicroserviceLogLevelContext.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Logging; + +namespace Beamable.Server +{ + public static class MicroserviceLogLevelContext + { + public static readonly AsyncLocal CurrentLogLevel = new(); + } + +} diff --git a/microservice/microservice/Api/RealmConfig/RealmConfigService.cs b/microservice/microservice/Api/RealmConfig/RealmConfigService.cs index c56f0b7cff..6bd7f6d324 100644 --- a/microservice/microservice/Api/RealmConfig/RealmConfigService.cs +++ b/microservice/microservice/Api/RealmConfig/RealmConfigService.cs @@ -40,7 +40,7 @@ public RealmConfigService(IBeamableRequester requester, SocketRequesterContext c public void UpdateLogLevel() { var level = GetLogLevel(); - MicroserviceBootstrapper.ContextLogLevel.Value = level; + MicroserviceLogLevelContext.CurrentLogLevel.Value = level; } private LogLevel GetLogLevel() diff --git a/microservice/microservice/dbmicroservice/BeamableMicroService.cs b/microservice/microservice/dbmicroservice/BeamableMicroService.cs index 7e9a94ba65..59d6952b0b 100644 --- a/microservice/microservice/dbmicroservice/BeamableMicroService.cs +++ b/microservice/microservice/dbmicroservice/BeamableMicroService.cs @@ -827,16 +827,18 @@ async Task HandleWebsocketMessage(IConnection ws, JsonDocument document, Stopwat // First get the Global Realm Config Log Level and apply it by running UpdateLogLevel var configService = InstanceArgs.ServiceScope.GetService(); - if (ctx.Path?.StartsWith(_adminPrefix) ?? false) - { - // when the path starts with admin, use warning. - MicroserviceBootstrapper.ContextLogLevel.Value = LogLevel.Warning; - } - else - { - // otherwise, allow default behaviour. - configService.UpdateLogLevel(); - } + // if (ctx.Path?.StartsWith(_adminPrefix) ?? false) + // { + // // when the path starts with admin, use warning. + // MicroserviceBootstrapper.ContextLogLevel.Value = LogLevel.Warning; + // } + // else + // { + // // otherwise, allow default behaviour. + // configService.UpdateLogLevel(); + // } + + configService.UpdateLogLevel(); string routingKey = InstanceArgs.GetRoutingKey().GetOrElse(string.Empty); try @@ -901,7 +903,7 @@ async Task HandleWebsocketMessage(IConnection ws, JsonDocument document, Stopwat } - MicroserviceBootstrapper.ContextLogLevel.Value = contextLogLevel; + MicroserviceLogLevelContext.CurrentLogLevel.Value = contextLogLevel; void TryToSetNewLogLevel(LogLevel newLogLevel) { diff --git a/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs b/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs index c57424af3e..0f4eec4006 100644 --- a/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs +++ b/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs @@ -17,7 +17,8 @@ public class SingleUseUserContext : IUserContext public static partial class MicroserviceBootstrapper { - public static AsyncLocal ContextLogLevel = new(); + [Obsolete("Use MicroserviceLogLevelContext.CurrentLogLevel instead.")] + public static AsyncLocal ContextLogLevel => MicroserviceLogLevelContext.CurrentLogLevel; private static BeamServiceConfigBuilder _preparedBuilder; diff --git a/microservice/microservice/dbmicroservice/MicroserviceStartupUtil.cs b/microservice/microservice/dbmicroservice/MicroserviceStartupUtil.cs index ff46621505..6f8094405f 100644 --- a/microservice/microservice/dbmicroservice/MicroserviceStartupUtil.cs +++ b/microservice/microservice/dbmicroservice/MicroserviceStartupUtil.cs @@ -102,7 +102,7 @@ public static async Task Begin(IBeamServiceConfig configurat if (startupCtx.IsGeneratingOapi) { LogUtil.TryParseSystemLogLevel(configuredArgs.OapiGenLogLevel, out var defaultLevel, LogLevel.Information); - MicroserviceBootstrapper.ContextLogLevel.Value = defaultLevel; + MicroserviceLogLevelContext.CurrentLogLevel.Value = defaultLevel; await GenerateOpenApiSpecification(startupCtx, configurator); startupCtx.result.GeneratedClient = true; return startupCtx.result; @@ -380,7 +380,7 @@ private static void ConfigureLogging(IBeamServiceConfig configurator, StartupCon defaultLogLevel = LogLevel.Warning; } - MicroserviceBootstrapper.ContextLogLevel.Value = defaultLogLevel; + MicroserviceLogLevelContext.CurrentLogLevel.Value = defaultLogLevel; var debugLogOptions = UseBeamJsonFormatter(new ZLoggerOptions()); ctx.debugLogProcessor = new DebugLogProcessor(debugLogOptions); @@ -392,7 +392,7 @@ private static void ConfigureLogging(IBeamServiceConfig configurator, StartupCon // all logs are valid, but may not pass the filter. builder.SetMinimumLevel(LogLevel.Trace); - builder.AddFilter(level => level >= MicroserviceBootstrapper.ContextLogLevel.Value); + builder.AddFilter(level => level >= MicroserviceLogLevelContext.CurrentLogLevel.Value); if (!ctx.InDocker) { builder.AddZLoggerLogProcessor(ctx.debugLogProcessor); From 5958ad254b5b2f8af7aa12606294696e7a51b932 Mon Sep 17 00:00:00 2001 From: moe Date: Mon, 4 May 2026 14:27:52 -0300 Subject: [PATCH 2/2] - Add configurable admin route log policy --- .../Microservice/AdminRoutes.cs | 32 +-- .../Microservice/IMicroserviceArgs.cs | 7 + .../Microservice/MicroserviceArgs.cs | 15 ++ .../dbmicroservice/BeamableMicroService.cs | 110 +++++---- .../OpenAPITests/DocTests.cs | 107 +++++++-- .../microservice/TestArgs.cs | 30 +-- .../AdminRouteLogPolicyTests.cs | 226 ++++++++++++++++++ 7 files changed, 437 insertions(+), 90 deletions(-) create mode 100644 microservice/microserviceTests/microservice/dbmicroservice/BeamableMicroServiceTests/AdminRouteLogPolicyTests.cs diff --git a/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs b/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs index 238cda7033..73a44d0f31 100644 --- a/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs +++ b/microservice/beamable.tooling.common/Microservice/AdminRoutes.cs @@ -84,16 +84,16 @@ public string HealthCheck() [CustomResponseSerializationAttribute] public string Docs() { - // Suppress info-level logs during OpenAPI doc generation to avoid noise - //Wrapping with try/finally to ensure log level is restored even if an exception occurs - var previousLogLevel = MicroserviceLogLevelContext.CurrentLogLevel.Value; - MicroserviceLogLevelContext.CurrentLogLevel.Value = LogLevel.Warning; - - try - { - var docs = new ServiceDocGenerator(); - var ctx = GlobalProvider.GetService(); - var doc = docs.Generate(ctx, GlobalProvider); + // Suppress info-level logs during OpenAPI doc generation to avoid noise + //Wrapping with try/finally to ensure log level is restored even if an exception occurs + var previousLogLevel = MicroserviceLogLevelContext.CurrentLogLevel.Value; + MicroserviceLogLevelContext.CurrentLogLevel.Value = LogLevel.Warning; + + try + { + var docs = new ServiceDocGenerator(); + var ctx = GlobalProvider.GetService(); + var doc = docs.Generate(ctx, GlobalProvider); if (!string.IsNullOrEmpty(PublicHost)) { @@ -104,12 +104,12 @@ public string Docs() return outputString; - } - finally - { - MicroserviceLogLevelContext.CurrentLogLevel.Value = previousLogLevel; - } - } + } + finally + { + MicroserviceLogLevelContext.CurrentLogLevel.Value = previousLogLevel; + } + } /// /// Fetch various Beamable SDK metadata for the Microservice diff --git a/microservice/beamable.tooling.common/Microservice/IMicroserviceArgs.cs b/microservice/beamable.tooling.common/Microservice/IMicroserviceArgs.cs index 412485fac6..fca1c7a5ec 100644 --- a/microservice/beamable.tooling.common/Microservice/IMicroserviceArgs.cs +++ b/microservice/beamable.tooling.common/Microservice/IMicroserviceArgs.cs @@ -8,6 +8,12 @@ public enum LogOutputType DEFAULT, STRUCTURED, UNSTRUCTURED, FILE, STRUCTURED_AND_FILE } +public enum AdminRouteLogPolicy +{ + Protected = 0, //Default where Admin logs are restricted to warning and above + FollowServiceRules = 1 //Admin logs follow service rules +} + public interface IMicroserviceArgs : IRealmInfo, IActivityProviderArgs { public IDependencyProviderScope ServiceScope { get; } @@ -59,4 +65,5 @@ public interface IMicroserviceArgs : IRealmInfo, IActivityProviderArgs public bool AllowStartupWithoutBeamableSettings { get; } public int MaxUniqueEventBindingCount { get; } void SetResolvedCid(string resolvedCid); + public AdminRouteLogPolicy AdminRouteLogPolicy { get; } } diff --git a/microservice/beamable.tooling.common/Microservice/MicroserviceArgs.cs b/microservice/beamable.tooling.common/Microservice/MicroserviceArgs.cs index d26da88847..48840fb7e3 100644 --- a/microservice/beamable.tooling.common/Microservice/MicroserviceArgs.cs +++ b/microservice/beamable.tooling.common/Microservice/MicroserviceArgs.cs @@ -68,6 +68,8 @@ public void SetResolvedCid(string resolvedCid) { throw new NotImplementedException(); } + + public AdminRouteLogPolicy AdminRouteLogPolicy { get; set; } } public static class MicroserviceArgsExtensions @@ -126,6 +128,7 @@ public static MicroserviceArgs Copy(this IMicroserviceArgs args, Action(rawPolicy, true, out var policy) ? + policy : + AdminRouteLogPolicy.Protected; + } + } + public string Host => Environment.GetEnvironmentVariable("HOST"); public string Secret => Environment.GetEnvironmentVariable("SECRET"); public string NamePrefix => Environment.GetEnvironmentVariable("NAME_PREFIX") ?? ""; diff --git a/microservice/microservice/dbmicroservice/BeamableMicroService.cs b/microservice/microservice/dbmicroservice/BeamableMicroService.cs index 59d6952b0b..a314c34963 100644 --- a/microservice/microservice/dbmicroservice/BeamableMicroService.cs +++ b/microservice/microservice/dbmicroservice/BeamableMicroService.cs @@ -794,52 +794,16 @@ async Task HandleClientMessage(MicroserviceRequestContext ctx, Stopwatch sw, Bea } } - async Task HandleWebsocketMessage(IConnection ws, JsonDocument document, Stopwatch sw) + /// + /// Applies logging context rules based on the provided request context. + /// This method utilizes the microservice's routing key and logging context service + /// to determine the appropriate log level and any additional logging rules to be applied. + /// It ensures that logging behavior is dynamically adjusted to reflect the specified configuration. + /// + /// The context of the current request, providing necessary information + /// for logging configuration, such as routing keys and associated metadata. + private void ApplyLoggingContextRules(RequestContext ctx) { - - MicroserviceRequestContext ctx = null; - using (var requestActivity = _activityProvider.Create(Otel.TRACE_CONSTRUCT_CTX, importance: TelemetryImportance.VERBOSE)) - { - if (!document.TryBuildRequestContext(InstanceArgs, out ctx)) - { - requestActivity.SetStatus(ActivityStatusCode.Error); - Log.Debug("WS Message contains no data. Cannot handle. Skipping message."); - return; - } - requestActivity.SetStatus(ActivityStatusCode.Ok); - } - - BeamActivity parentActivity = BeamActivity.Noop; - if (_socketRequesterContext.TryGetListener(ctx.Id, out var existingListener)) - { - parentActivity = existingListener.Activity; - } - - - using var activity = _activityProvider.Create(Otel.TRACE_WS, parentActivity); - MicroserviceRequester.ContextActivity.Value = activity; - ctx.ActivityContext = activity; - - - var extraOtelTags = _telemetryProviders.CreateRequestAttributes(InstanceArgs, ctx, ConnectionId); - activity.SetTags(extraOtelTags); - - - // First get the Global Realm Config Log Level and apply it by running UpdateLogLevel - var configService = InstanceArgs.ServiceScope.GetService(); - // if (ctx.Path?.StartsWith(_adminPrefix) ?? false) - // { - // // when the path starts with admin, use warning. - // MicroserviceBootstrapper.ContextLogLevel.Value = LogLevel.Warning; - // } - // else - // { - // // otherwise, allow default behaviour. - // configService.UpdateLogLevel(); - // } - - configService.UpdateLogLevel(); - string routingKey = InstanceArgs.GetRoutingKey().GetOrElse(string.Empty); try { @@ -920,7 +884,63 @@ void TryToSetNewLogLevel(LogLevel newLogLevel) { BeamableZLoggerProvider.Instance.Error(ex); } + } + + /// + /// Adjusts the log level for the current request context based on specific policy criteria. + /// Determines whether the request is for a protected admin route and sets the log level accordingly. + /// If the route is not protected, applies custom logging rules and updates the log level + /// using the service configuration. + /// + /// The request context object containing information about the current request, + /// including the request path. + private void ApplyRequestLogLevel(RequestContext ctx) + { + var isProtectedAdminRoute = ctx.Path?.StartsWith(_adminPrefix) == true + && InstanceArgs.AdminRouteLogPolicy == AdminRouteLogPolicy.Protected; + + if (isProtectedAdminRoute) + { + MicroserviceLogLevelContext.CurrentLogLevel.Value = LogLevel.Warning; + } + else + { + var configureService = InstanceArgs.ServiceScope.GetService(); + configureService.UpdateLogLevel(); + ApplyLoggingContextRules(ctx); + } + } + + async Task HandleWebsocketMessage(IConnection ws, JsonDocument document, Stopwatch sw) + { + MicroserviceRequestContext ctx = null; + using (var requestActivity = _activityProvider.Create(Otel.TRACE_CONSTRUCT_CTX, importance: TelemetryImportance.VERBOSE)) + { + if (!document.TryBuildRequestContext(InstanceArgs, out ctx)) + { + requestActivity.SetStatus(ActivityStatusCode.Error); + Log.Debug("WS Message contains no data. Cannot handle. Skipping message."); + return; + } + requestActivity.SetStatus(ActivityStatusCode.Ok); + } + + BeamActivity parentActivity = BeamActivity.Noop; + if (_socketRequesterContext.TryGetListener(ctx.Id, out var existingListener)) + { + parentActivity = existingListener.Activity; + } + + + using var activity = _activityProvider.Create(Otel.TRACE_WS, parentActivity); + MicroserviceRequester.ContextActivity.Value = activity; + ctx.ActivityContext = activity; + + + var extraOtelTags = _telemetryProviders.CreateRequestAttributes(InstanceArgs, ctx, ConnectionId); + activity.SetTags(extraOtelTags); + ApplyRequestLogLevel(ctx); var logger = BeamableZLoggerProvider.LogContext.Value = InstanceArgs.ServiceScope.GetLogger(); using var scope = logger.BeginScope(extraOtelTags.ToDictionary()); diff --git a/microservice/microserviceTests/OpenAPITests/DocTests.cs b/microservice/microserviceTests/OpenAPITests/DocTests.cs index d20259b309..bbbc17c44d 100644 --- a/microservice/microserviceTests/OpenAPITests/DocTests.cs +++ b/microservice/microserviceTests/OpenAPITests/DocTests.cs @@ -11,17 +11,19 @@ using NUnit.Framework; using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Beamable.Common.Dependencies; -using microserviceTests.microservice.Util; +using System.Threading.Tasks; +using Beamable.Common.Dependencies; +using microserviceTests.microservice.Util; +using microservice.Common; +using Microsoft.Extensions.Logging; namespace microserviceTests.OpenAPITests; [Serializable] public class DocTests { - public class ExampleAttrs : ITelemetryAttributeProvider - { + public class ExampleAttrs : ITelemetryAttributeProvider + { public List GetDescriptors() { return new List @@ -47,11 +49,34 @@ public void CreateConnectionAttributes(IConnectionAttributeContext ctx) public void CreateRequestAttributes(IRequestAttributeContext ctx) { - } - } - - [Test] - public void TestMethodScanning() + } + } + + public class RecordingTelemetryAttributeProvider : ITelemetryAttributeProvider + { + public static readonly List SeenLogLevels = new List(); + + public List GetDescriptors() + { + SeenLogLevels.Add(MicroserviceLogLevelContext.CurrentLogLevel.Value); + return new List(); + } + + public void CreateDefaultAttributes(IDefaultAttributeContext ctx) + { + } + + public void CreateConnectionAttributes(IConnectionAttributeContext ctx) + { + } + + public void CreateRequestAttributes(IRequestAttributeContext ctx) + { + } + } + + [Test] + public void TestMethodScanning() { LoggingUtil.InitTestCorrelator(); var gen = new ServiceDocGenerator(); @@ -72,11 +97,63 @@ public void TestMethodScanning() // Assert.AreEqual(1, reqSchema.Required.Count); var outputString = doc.Serialize(OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); - Console.WriteLine(outputString); - } - - [Microservice("docs")] - public class DocService : Microservice + Console.WriteLine(outputString); + } + + [Test] + public void AdminRoutesDocsTemporarilyRaisesLogLevelAndRestoresPreviousValue() + { + LoggingUtil.InitTestCorrelator(); + RecordingTelemetryAttributeProvider.SeenLogLevels.Clear(); + + var originalLogLevel = MicroserviceLogLevelContext.CurrentLogLevel.Value; + var previousLogLevel = LogLevel.Debug; + MicroserviceLogLevelContext.CurrentLogLevel.Value = previousLogLevel; + + try + { + var args = new TestArgs(); + var microserviceAttribute = new MicroserviceAttribute("docs"); + var startupContext = new StartupContext + { + args = args, + attributes = microserviceAttribute, + routeSources = new[] + { + new BeamRouteSource + { + InstanceType = typeof(DocService) + } + } + }; + + var builder = new DependencyBuilder(); + builder.AddSingleton(args); + builder.AddSingleton(startupContext); + builder.AddSingleton>(); + builder.AddSingleton(); + + var routes = new AdminRoutes + { + GlobalProvider = builder.Build(), + MicroserviceAttribute = microserviceAttribute + }; + + var outputString = routes.Docs(); + + Assert.That(outputString, Does.Contain("\"openapi\"")); + Assert.That(RecordingTelemetryAttributeProvider.SeenLogLevels, Is.Not.Empty); + Assert.That(RecordingTelemetryAttributeProvider.SeenLogLevels, Has.All.EqualTo(LogLevel.Warning)); + Assert.AreEqual(previousLogLevel, MicroserviceLogLevelContext.CurrentLogLevel.Value); + } + finally + { + MicroserviceLogLevelContext.CurrentLogLevel.Value = originalLogLevel; + } + } + + [Microservice("docs")] + public class DocService : Microservice { [ServerCallable] diff --git a/microservice/microserviceTests/microservice/TestArgs.cs b/microservice/microserviceTests/microservice/TestArgs.cs index 926a41ae95..05aa74aaee 100644 --- a/microservice/microserviceTests/microservice/TestArgs.cs +++ b/microservice/microserviceTests/microservice/TestArgs.cs @@ -29,9 +29,9 @@ public TestSetup(IConnectionProvider provider, IContentResolver resolver=null) _provider = provider; } - public async Task Start(TestArgs dudArgs=null, Action configurator=null) where T : Microservice - { - var args = new TestArgs(); + public async Task Start(TestArgs dudArgs=null, Action configurator=null) where T : Microservice + { + var args = dudArgs ?? new TestArgs(); var attr = typeof(T).GetCustomAttribute(); @@ -171,14 +171,16 @@ public class TestArgs : IMicroserviceArgs public bool OtelExporterShouldRetry { get; } public bool OtelExporterStandardEnabled => false; public string OtelExporterRetryMaxSize { get; } - public bool AllowStartupWithoutBeamableSettings => false; - public int MaxUniqueEventBindingCount => 100; - public bool SkipLocalEnv => true; - public bool SkipAliasResolve => true; - - public void SetResolvedCid(string resolvedCid) - { - throw new NotImplementedException(); - } - } -} + public bool AllowStartupWithoutBeamableSettings => false; + public int MaxUniqueEventBindingCount => 100; + public bool SkipLocalEnv => true; + public bool SkipAliasResolve => true; + public AdminRouteLogPolicy AdminRouteLogPolicy { get; set; } = AdminRouteLogPolicy.Protected; + + public void SetResolvedCid(string resolvedCid) + { + throw new NotImplementedException(); + } + + } +} diff --git a/microservice/microserviceTests/microservice/dbmicroservice/BeamableMicroServiceTests/AdminRouteLogPolicyTests.cs b/microservice/microserviceTests/microservice/dbmicroservice/BeamableMicroServiceTests/AdminRouteLogPolicyTests.cs new file mode 100644 index 0000000000..9b61fcc58e --- /dev/null +++ b/microservice/microserviceTests/microservice/dbmicroservice/BeamableMicroServiceTests/AdminRouteLogPolicyTests.cs @@ -0,0 +1,226 @@ +using Beamable.Api.Autogenerated.Models; +using Beamable.Common; +using Beamable.Common.Dependencies; +using Beamable.Microservice.Tests.Socket; +using Beamable.Server; +using Beamable.Server.Api.Logs; +using Beamable.Server.Api.RealmConfig; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ClientRequest = Beamable.Microservice.Tests.Socket.ClientRequest; + +namespace microserviceTests.microservice.dbmicroservice.BeamableMicroServiceTests; + +[TestFixture] +public class AdminRouteLogPolicyTests : CommonTest +{ + private const string AdminRouteLogPolicyEnvVar = "BEAM_ADMIN_ROUTE_LOG_POLICY"; + + [TestCase(null, AdminRouteLogPolicy.Protected)] + [TestCase("", AdminRouteLogPolicy.Protected)] + [TestCase("Protected", AdminRouteLogPolicy.Protected)] + [TestCase("FollowServiceRules", AdminRouteLogPolicy.FollowServiceRules)] + [TestCase("followservicerules", AdminRouteLogPolicy.FollowServiceRules)] + [TestCase("NotARealPolicy", AdminRouteLogPolicy.Protected)] + [NonParallelizable] + public void EnvironmentArgsParsesAdminRouteLogPolicy(string rawPolicy, AdminRouteLogPolicy expectedPolicy) + { + var previousPolicy = Environment.GetEnvironmentVariable(AdminRouteLogPolicyEnvVar); + try + { + Environment.SetEnvironmentVariable(AdminRouteLogPolicyEnvVar, rawPolicy); + var args = new EnvironmentArgs(); + + Assert.AreEqual(expectedPolicy, args.AdminRouteLogPolicy); + } + finally + { + Environment.SetEnvironmentVariable(AdminRouteLogPolicyEnvVar, previousPolicy); + } + } + + [Test] + [NonParallelizable] + public async Task ProtectedAdminRouteDoesNotApplyServiceLogRules() + { + var realmConfig = new RecordingRealmConfigService(LogLevel.Debug); + var loggingContext = new RecordingLoggingContextService(); + TestSocket testSocket = null; + + var ms = new TestSetup(new TestSocketProvider(socket => + { + testSocket = socket; + socket + .AddStandardMessageHandlers() + .AddMessageHandler( + MessageMatcher + .WithReqId(1) + .WithStatus(200) + .WithPayload(payload => payload == "responsive"), + MessageResponder.NoResponse(), + MessageFrequency.OnlyOnce()); + })); + + await ms.Start(new TestArgs + { + AdminRouteLogPolicy = AdminRouteLogPolicy.Protected + }, builder => RegisterLogServices(builder, realmConfig, loggingContext)); + + Assert.IsTrue(ms.HasInitialized); + var updateLogLevelCount = realmConfig.UpdateLogLevelCount; + var getLogLevelContextCount = loggingContext.GetLogLevelContextCount; + + testSocket.SendToClient(ClientRequest.Callable("micro_log_policy", "admin/HealthCheck", 1)); + await Task.Delay(50); + + Assert.AreEqual(updateLogLevelCount, realmConfig.UpdateLogLevelCount); + Assert.AreEqual(getLogLevelContextCount, loggingContext.GetLogLevelContextCount); + + await ms.OnShutdown(this, null); + Assert.IsTrue(testSocket.AllMocksCalled()); + } + + [Test] + [NonParallelizable] + public async Task FollowServiceRulesAdminRouteAppliesServiceLogRules() + { + var realmConfig = new RecordingRealmConfigService(LogLevel.Debug); + var loggingContext = new RecordingLoggingContextService(); + TestSocket testSocket = null; + + var ms = new TestSetup(new TestSocketProvider(socket => + { + testSocket = socket; + socket + .AddStandardMessageHandlers() + .AddMessageHandler( + MessageMatcher + .WithReqId(1) + .WithStatus(200) + .WithPayload(payload => payload == "responsive"), + MessageResponder.NoResponse(), + MessageFrequency.OnlyOnce()); + })); + + await ms.Start(new TestArgs + { + AdminRouteLogPolicy = AdminRouteLogPolicy.FollowServiceRules + }, builder => RegisterLogServices(builder, realmConfig, loggingContext)); + + Assert.IsTrue(ms.HasInitialized); + var updateLogLevelCount = realmConfig.UpdateLogLevelCount; + var getLogLevelContextCount = loggingContext.GetLogLevelContextCount; + + testSocket.SendToClient(ClientRequest.Callable("micro_log_policy", "admin/HealthCheck", 1)); + await Task.Delay(50); + + Assert.AreEqual(updateLogLevelCount + 1, realmConfig.UpdateLogLevelCount); + Assert.AreEqual(getLogLevelContextCount + 1, loggingContext.GetLogLevelContextCount); + + await ms.OnShutdown(this, null); + Assert.IsTrue(testSocket.AllMocksCalled()); + } + + [Test] + [NonParallelizable] + public async Task ProtectedPolicyStillAppliesServiceLogRulesForNonAdminRoute() + { + var realmConfig = new RecordingRealmConfigService(LogLevel.Debug); + var loggingContext = new RecordingLoggingContextService(); + TestSocket testSocket = null; + + var ms = new TestSetup(new TestSocketProvider(socket => + { + testSocket = socket; + socket + .AddStandardMessageHandlers() + .AddMessageHandler( + MessageMatcher + .WithReqId(1) + .WithStatus(200) + .WithPayload(payload => payload == "ok"), + MessageResponder.NoResponse(), + MessageFrequency.OnlyOnce()); + })); + + await ms.Start(new TestArgs + { + AdminRouteLogPolicy = AdminRouteLogPolicy.Protected + }, builder => RegisterLogServices(builder, realmConfig, loggingContext)); + + Assert.IsTrue(ms.HasInitialized); + var updateLogLevelCount = realmConfig.UpdateLogLevelCount; + var getLogLevelContextCount = loggingContext.GetLogLevelContextCount; + + testSocket.SendToClient(ClientRequest.ClientCallable("micro_log_policy", nameof(LogPolicyService.Ping), 1, 1)); + await Task.Delay(50); + + Assert.AreEqual(updateLogLevelCount + 1, realmConfig.UpdateLogLevelCount); + Assert.AreEqual(getLogLevelContextCount + 1, loggingContext.GetLogLevelContextCount); + + await ms.OnShutdown(this, null); + Assert.IsTrue(testSocket.AllMocksCalled()); + } + + private static void RegisterLogServices(IDependencyBuilder builder, RecordingRealmConfigService realmConfig, RecordingLoggingContextService loggingContext) + { + builder.RemoveIfExists(); + builder.RemoveIfExists(); + builder.RemoveIfExists(); + builder.AddSingleton(realmConfig); + builder.AddSingleton(realmConfig); + builder.AddSingleton(loggingContext); + } + + private class RecordingRealmConfigService : IRealmConfigService + { + private readonly LogLevel _level; + + public RecordingRealmConfigService(LogLevel level) + { + _level = level; + } + + public int UpdateLogLevelCount { get; private set; } + + public Promise GetRealmConfigSettings() + { + return Promise.Successful(new RealmConfig(new Dictionary())); + } + + public void UpdateLogLevel() + { + UpdateLogLevelCount++; + MicroserviceLogLevelContext.CurrentLogLevel.Value = _level; + } + } + + private class RecordingLoggingContextService : ILoggingContextService + { + public int GetLogLevelContextCount { get; private set; } + + public Promise> GetAllLoggingContexts() + { + return Promise>.Successful(new Dictionary()); + } + + public BeamoV2ServiceLoggingContext GetLogLevelContext(string serviceName, string routingKey) + { + GetLogLevelContextCount++; + return null; + } + } + + [Microservice("log_policy", EnableEagerContentLoading = false)] + public class LogPolicyService : Microservice + { + [ClientCallable] + public string Ping() + { + return "ok"; + } + } +}