-
Notifications
You must be signed in to change notification settings - Fork 244
Custom receivers
Seelog tries to provide you with the most popular receiver options like: file (simple/rolling), console, network connection, etc.
But sometimes you run into a situation where you either have non-seelog logging system and want to use them together (e.g. AppEngine) or you want to implement another receiver (e.g. syslog).
To provide you with the possibility to implement another receiver or create an adapter for another logging system, seelog has different custom functions and techniques.
There are different scenarios with custom receivers/adapters and seelog provides different methods for each of them.
This is the scenario when you use seelog in all your code, but under some circumstances, want to just redirect all its output to another logging system. For example, you'd like to leave seelog calls there to be able to return to its functionality later, but currently you would like to just use it as a proxy.
In this scenario, seelog doesn't use any of its filter/exception/dispatching functionality and just stands as a proxy.
There are two sub-cases of this scenario:
In this scenario, you redirect all seelog output to something really simple, that can be fully described by the io.Writer interface. For such cases, you should use seelog.LoggerFromWriterWithMinLevelAndFormat
. It takes an io.Writer, format, and a minimal log level as the only parameters. Minimal log level gives you the possibility to have production/development builds with minimal level set to 'info' or 'trace' accordingly.
Usage example:
In this example we have a simple io.Writer and just proxy all seelog output to it. Here we emulate the 'production' situation, so minimal level is set to 'info'.
package main
import (
"fmt"
seelog "github.com/cihub/seelog"
)
type SomeWriter struct{}
func (sw *SomeWriter) Write(p []byte) (n int, err error) {
fmt.Println(string(p))
return len(p), nil
}
func main() {
log, err := seelog.LoggerFromWriterWithMinLevelAndFormat(&SomeWriter{}, seelog.InfoLvl, "%Ns [%Level] %Msg")
if err != nil {
panic(err)
}
defer log.Flush()
log.Debug("Test")
log.Info("Test2")
}
Output:
1381334355481209992 [Info] Test2
This scenario is similar to the previous one, but here you need to log to a logging subsystem that has its own levels, rules, or special actions.
In this situation you use LoggerFromCustomReceiver
that takes a custom receiver which implements seelog.CustomReceiver
interface.
Usage example:
Let's pretend that we use a logging subsystem that logs using 3 log levels and a function info:
type SomeLogger struct{}
func (sw *SomeLogger) Debug(fromFunc, s string) {
fmt.Printf("DEBUG from %s: %s\n", fromFunc, s)
}
func (sw *SomeLogger) Info(fromFunc, s string) {
fmt.Printf("INFO from %s: %s\n", fromFunc, s)
}
func (sw *SomeLogger) Error(fromFunc, s string) {
fmt.Printf("ERROR from %s: %s\n", fromFunc, s)
}
Now we want to proxy seelog to it somehow. So we need to redirect seelog.Trace + seelog.Debug to SomeLogger.Debug, seelog.Info to SomeLogger.Info, and seelog.Warn + seelog.Error + seelog.Critical to SomeLogger.Error. Also we want to pass the caller function information.
To do this we create a custom receiver which implements seelog.CustomReceiver
and proxies everything to SomeLogger
in a way that is required:
type SomeCustomReceiver struct {
l *SomeLogger
}
func (ar *SomeCustomReceiver) ReceiveMessage(message string, level seelog.LogLevel, context seelog.LogContextInterface) error {
switch level {
case seelog.TraceLvl:
fallthrough
case seelog.DebugLvl:
ar.l.Debug(context.Func(), message)
case seelog.InfoLvl:
ar.l.Info(context.Func(), message)
case seelog.WarnLvl:
fallthrough
case seelog.ErrorLvl:
fallthrough
case seelog.CriticalLvl:
ar.l.Error(context.Func(), message)
}
return nil
}
/* NOTE: NOT called when LoggerFromCustomReceiver is used */
func (ar *SomeCustomReceiver) AfterParse(initArgs seelog.CustomReceiverInitArgs) error {
return nil
}
func (ar *SomeCustomReceiver) Flush() {
}
func (ar *SomeCustomReceiver) Close() error {
return nil
}
func main() {
log, err := seelog.LoggerFromCustomReceiver(&SomeCustomReceiver{&SomeLogger{}})
if err != nil {
panic(err)
}
defer log.Flush()
log.Debug("Test")
log.Info("Test2")
}
Output:
DEBUG from main.main: Test
INFO from main.main: Test2
This is the scenario when you have your own logging subsystem/component and you want to redirect its log stream to seelog. This is the opposite situation to Scenario 1.
In this scenario you just call seelog functions from your other logging subsystem and call SetAdditionalStackDepth
to detect actual caller func (not the subsystem one). The latter will be explained using the following example.
Usage example:
package main
import (
seelog "github.com/cihub/seelog"
)
type SomeLogger struct {
inner seelog.LoggerInterface
}
func (sw *SomeLogger) Debug(s string) {
sw.inner.Debug(s)
}
func (sw *SomeLogger) Info(s string) {
sw.inner.Info(s)
}
func (sw *SomeLogger) Error(s string) {
sw.inner.Error(s)
}
var log = &SomeLogger{}
func init() {
var err error
log.inner, err = seelog.LoggerFromConfigAsString(
`<seelog>
<outputs>
<console formatid="fmt"/>
</outputs>
<formats>
<format id="fmt" format="[%Func] [%Lev] %Msg%n"/>
</formats>
</seelog>
`)
if err != nil {
panic(err)
}
log.inner.SetAdditionalStackDepth(1)
}
func main() {
defer log.inner.Flush()
log.Debug("Test")
log.Info("Test2")
}
Output:
[main.main] [Dbg] Test
[main.main] [Inf] Test2
To get the idea of SetAdditionalStackDepth
lets pretend that it is not called or is called with argument = 0 instead of 1. In that case the output is:
[main.(*SomeLogger).Debug] [Dbg] Test
[main.(*SomeLogger).Info] [Inf] Test2
It is actually valid output because these are the functions where seelog was called. But in current scenario you are actually redirecting your subsystem log messages to seelog, so actually you need to know where your subsystem func was called, not the seelog one. That's why SetAdditionalStackDepth
is used. Its argument is set to the additional number of caller stack frames to skip to get to the actual caller func.
In this scenario you utilize all the capabilities of seelog AND you add additional custom receivers that may be used in seelog config alongside others.
There are two sub-scenarios here.
This is the case when custom receiver doesn't depend on any other variables/parameters in code or if those dependencies are set one time and doesn't change. However it may depend on attributes in config. This will be explained by the following example.
Usage example:
Let's try to create a custom receiver that logs everything using fmt.Printf, but adds a custom prefix. We need to implement seelog.CustomReceiver
to describe this type of receiver.
type CustomReceiver struct { // implements seelog.CustomReceiver
prefix string // Just an example. Any data may be present
}
func (ar *CustomReceiver) ReceiveMessage(message string, level log.LogLevel, context log.LogContextInterface) error {
fmt.Printf("[%s].%s", ar.prefix, message) // Just log message, but prepend a prefix
return nil
}
func (ar *CustomReceiver) AfterParse(initArgs log.CustomReceiverInitArgs) error {
var ok bool
ar.prefix, ok = initArgs.XmlCustomAttrs["prefix"]
if !ok {
ar.prefix = "No prefix"
}
return nil
}
func (ar *CustomReceiver) Flush() {
}
func (ar *CustomReceiver) Close() error {
return nil
}
Check the AfterParse
implementation. Unlike Scenario 1.2, here it is going to be called, because here we are actually going to parse a config. initArgs.XmlCustomAttrs
contains map of values of <custom>
attributes beginning with data-
prefix (the prefix won't be included in map keys).
Now we should register this type:
log.RegisterReceiver("myreceiver", &CustomReceiver{})
Now we ready to use it. Let's create a couple of these receivers with different prefixes!
testConfig := `
<seelog>
<outputs>
<custom name="myreceiver" formatid="test"/>
<custom name="myreceiver" formatid="test" data-prefix="CustomPrefix"/>
<console formatid="test"/>
</outputs>
<formats>
<format id="test" format="%Func %LEV %Msg%n"/>
</formats>
</seelog>
`
logger, err := log.LoggerFromConfigAsBytes([]byte(testConfig))
if err != nil {
panic(err)
}
defer log.Flush()
err = log.ReplaceLogger(logger)
if err != nil {
panic(err)
}
After this, two instances of CustomReceiver
will be created and AfterParse
will be called for both of them.
Now we have 3 receivers: console, myreceiver (mapped to CustomReceiver) without prefix and myreceiver with prefix="CustomPrefix". So, if we log something:
log.Debug("Test1")
log.Info("Test2")
we get the following output:
main.main DBG Test1
[No prefix].main.main DBG Test1
[CustomPrefix].main.main DBG Test1
main.main INF Test2
[No prefix].main.main INF Test2
[CustomPrefix].main.main INF Test2
Note that now we are actually a part of seelog infrastructure (unlike Scenario 1, where seelog was just a proxy), so all formatting/filtering/other rules are applied. For instance, CustomReceiver gets message = "main.main DBG Test1" because its format id in config was set to 'test', defined as:
<format id="test" format="%Func %LEV %Msg%n"/>