The OneStream Community is temporarily frozen until June 29th due to the ongoing maintenance. Please read the blog post here to learn more.
Forum Discussion
ludodepaz
OneStream Employee
2 years ago[How to] Log into a file instead of the Error Log
Logging with OneStream is great but when we all use the Error Log at the same time, things can get messy very quickly. Moreover, logging in a file instead of the Error Log can be very convenient when logging kickouts.
The initial setup can be a little involving but once you get a knack out of it, logging in a file is as easy as using the Error Log.
In this post, I will show you how to do the initial setup and then how to log into a file instead of the Error Log only when there is an exception or every time.
This rule and methodology is the result of the genius work of Matt Ha and I am very thankful he shared it with us.
Initial Setup
In order to log into a file, you need to import a Public Business Rule, it will be called by the logger and it is available in this post (GS_GlobalHelper.xml).
Side note, remember that in order to make a BR Public, you need to change the setting for ‘Contains Global Functions for Formulas’ to True.
Setup of the BR to log into a file
In order to call a Public function, you need to Reference it in your Business Rule
You also need to add it to your Imports:
Imports OneStream.BusinessRule.Finance.GS_GlobalHelper.MainClassThe Logger function needs 2 variables to be declared and set, I like to do it at the very top of my rule
#Region "Logging Variables"
Dim bVerboseLogging As String = True
Dim logger As New Text.StringBuilder
#End Region
Then, you need to add 2 Private Functions to your rule, this is where you set your naming convention (if you want to add the data and time or anything else) and the folder name where you want your files to be stored.
Private Sub StoreLoggerOnSession(ByVal si As SessionInfo, ByVal api As FinanceRulesApi, ByRef globals As BRGlobals)
' Multithread-safe way to store logger to session
Dim globalLogger As Text.StringBuilder = globals.GetObject("globalLogger")
If globalLogger Is Nothing Then
globals.SetObject("globalLogger", logger)
Else
globalLogger.AppendLine(logger.ToString)
End If
End Sub
Private Sub WriteSessionLogToFileShare(ByVal si As SessionInfo, ByVal api As FinanceRulesApi, ByRef globals As BRGlobals, ByVal logName As String)
' Write logger stored on session to application database
Dim globalLogger As Text.StringBuilder = globals.GetObject("globalLogger")
If globalLogger IsNot Nothing Then
WriteLogger(si, api, globalLogger, logName, "TestLogs", DateTime.Now.ToString("yyyy-MM-dd-hh-mm"))
End If
End Sub
In the end, your rule should look like this:
Logging into a file when there is an Exception
You can write to a file instead of the Error log when an Exception happens, in this case, update the Exception Catcher at the end of your Main Function.
Catch ex As Exception
logger.AppendLine(ex.Message)
If bVerboseLogging Then StoreLoggerOnSession(si, api, globals)
WriteSessionLogToFileShare(si, api, globals, "MyRule")
Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
End TryLogging anything into a file
Of course, you can also log anything you would usually log to the Error Log in a file instead. Use the “logger” instead of the Error Log.
'Log to the Error Log
api.LogMessage("Logging to the Error Log with the api")
brapi.ErrorLog.LogMessage(si, "Logging to the Error Log with the brapi")
'Log to a file
logger.AppendLine("Logging to a file").AppendLine
#Region "Push logs to the File Explorer"
If bVerboseLogging Then StoreLoggerOnSession(si, api, globals)
WriteSessionLogToFileShare(si, api, globals, "MyRule")
#End RegionResults
This is what you get in the Error Log
And this is what you get in the File Explorer:
5 Replies
- MarkusWippContributor
Hi all,
I share my solution to it (this still logs to the standard OS logs):
You can put it into an extender business rule as a separate namespace in this case.
Namespace Common.Logging Public Interface ILogging Sub LogTrace(message As String) Sub LogDebug(message As String) Sub LogError(message As String) Sub LogMessage(message As String) End Interface Public Enum LogLevel LogTrace = 4 LogDebug = 3 LogMessage = 2 LogError = 1 End Enum Public Class OneStreamLogging Inherits DefaultLogging Public Const defaultMessageHeading = "OneStreamLogging Messages" Private si As SessionInfo Private userInLoggingGroup As Boolean Private collectLogs As Boolean Private collectedLogs As New Text.StringBuilder() Private messageHeading As String Public Sub New(ByVal si As SessionInfo, Optional logLevel As LogLevel = LogLevel.LogError, Optional withTiming As Boolean = False, Optional collectLogs As Boolean = False, Optional messageHeading As String = defaultMessageHeading) MyBase.New(logLevel, withTiming) Me.si = si Me.collectLogs = collectLogs Me.userInLoggingGroup = BRApi.Security.Authorization.IsUserInGroup(si, si.UserName, "ALLC_LoggingEnabledUsers", False) Me.messageHeading = messageHeading End Sub Protected Overrides Sub WriteMessage(loglevel As LogLevel, message As String) If loglevel.Equals(LogLevel.LogError) Then If collectLogs Then collectedLogs.AppendLine($"{loglevel}: {message}") Else BRApi.ErrorLog.LogError(si, New XFException(message)) End If Else If userInLoggingGroup Then If collectLogs Then collectedLogs.AppendLine($"{loglevel}: {message}") Else If messageHeading.Equals(defaultMessageHeading) Then BRApi.ErrorLog.LogMessage(si, message) Else BRApi.ErrorLog.LogMessage(si, Me.messageHeading, message) End If End If End If End If End Sub Public Sub LogCollectedLogs() If collectedLogs.Length > 0 Then If messageHeading.Equals(defaultMessageHeading) Then BRApi.ErrorLog.LogMessage(si, collectedLogs.ToString()) Else BRApi.ErrorLog.LogMessage(si, Me.messageHeading, collectedLogs.ToString()) End If End If End Sub End Class Public Class DefaultLogging Implements ILogging Private desiredLogLevel As LogLevel Private withTiming As Boolean Private elapsedTime As New Stopwatch() Public Sub New(Optional logLevel As LogLevel = LogLevel.LogError, Optional withTiming As Boolean = False) Me.desiredLogLevel = logLevel Me.withTiming = withTiming If Me.withTiming Then elapsedTime.Start() End If Me.LogTrace("Initialized Logger") End Sub Public Sub LogTrace(message As String) Implements ILogging.LogTrace Log(LogLevel.LogTrace, "TRACE", message) End Sub Public Sub LogDebug(message As String) Implements ILogging.LogDebug Log(LogLevel.LogDebug, "DEBUG", message) End Sub Public Sub LogError(message As String) Implements ILogging.LogError Log(LogLevel.LogError, "ERROR", message) End Sub Public Sub LogMessage(message As String) Implements ILogging.LogMessage Log(LogLevel.LogMessage, "MESSAGE", message) End Sub Private Sub Log(loglevel As LogLevel, levelname As String, message As String) If (loglevel <= Me.desiredLogLevel) Then Dim stackframe As New Diagnostics.StackFrame(2) Dim baseFormat = $"{levelname}: {stackframe.GetMethod().DeclaringType.Namespace}:{stackframe.GetMethod().DeclaringType.Name}:{stackframe.GetMethod().Name}: " If withTiming Then Dim ts As TimeSpan = elapsedTime.Elapsed Dim formattedElapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10) WriteMessage(loglevel, $"{baseFormat}: {formattedElapsedTime}: {message}") Else WriteMessage(loglevel, $"{baseFormat}: {message}") End If End If End Sub Protected Overridable Sub WriteMessage(loglevel As LogLevel, message As String) Console.WriteLine(message) End Sub End Class End Namespaceand then use e.g. like this from other BRs (of course you have to reference the above Business Rule
Imports Common.Logging Namespace OneStream.BusinessRule.Extender.Test Public Class MainClass Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As Object, ByVal args As DashboardStringFunctionArgs) As Object Dim logger As OneStreamLogging = New OneStreamLogging(si, LogLevel.LogDebug, True, True, $"Test Logging") Try logger.LogMessage("This is a message") logger.LogDebug("This is a debug message") logger.LogTrace("This is a trace massage") logger.LogError("This is an error message") Catch ex As Exception Throw ErrorHandler.LogWrite(si, New XFException(si, ex)) Finally logger.LogCollectedLogs() End Try End Function End Class End NamespaceThis allows you the follwoing:
- can be used outside onestream and logs to the console
- only log for users in security group ALLC_LoggingEnabledUsers
- only log messages up to e certain log-level
- collect logs during an execution and only log everything at once on the end
- logs additional useful information / timing / stack trace /etc.
Hope that is useful to some of you
Markus
- Sergey
OneStream Employee
This is neat (also a little bit more script than a regular brapi.errorlog.logmessage 😁 ).
When using it, did you see any differences in the time to log informations, using this method vs. logmessage ?
Regards,
- ludodepaz
OneStream Employee
Hi Sergey,
when we think about it, every time you hit the Error Log, you write a row in a table, this is very costly. With this solution, you put all logs in memory and then write a file so in the end, it's actually faster, especially if you write kick outs and have a lot of them.
- JackLacava
OneStream Employee
Thanks Ludo, this is very appreciated.
The issue of advanced logging came up a few times at Splash EMEA, it's a common issue for complex applications. Your solution is cool but makes some debatable choices (logs in a public folder, no protection against mistakenly keeping it on in Production, no on-off debug switch...); my feeling is that this sort of thing should be baked into the product, with better defaults that we have now. At the moment, every developer has to reinvent the wheel, with various degrees of success.
If you feel like, you could post a request on IdeaStream for more fine-grained logging, I'm sure it would gain support. You could also consider publishing this on OpenPlace.
- ludodepaz
OneStream Employee
Hi Jack,
that's an excellent idea, I will do that. I know some partners in the US do it on every project.
Related Content
- 2 months ago
- 2 years ago
- 1 month ago
- 4 years ago