Forum Discussion

ludodepaz's avatar
ludodepaz
Contributor
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.MainClass

The 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 Try

Logging 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 Region

Results

This is what you get in the Error Log


And this is what you get in the File Explorer:
  • 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 Namespace

     

     

    and 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 Namespace

     

     

     

    This 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's avatar
    Sergey
    Contributor III

    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's avatar
      ludodepaz
      Contributor

      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's avatar
    JackLacava
    Honored Contributor

    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's avatar
      ludodepaz
      Contributor

      Hi Jack,

      that's an excellent idea, I will do that. I know some partners in the US do it on every project.