Skip to content

Custom receivers

goodsign edited this page Oct 9, 2013 · 15 revisions

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.

Custom scenarios

There are different scenarios with custom receivers/adapters and seelog provides different methods for each of them.

Scenario 1: seelog as a proxy

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:

Scenario 1.1: Proxy to a simple writer

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

Scenario 1.2: Proxy to a more complex log system

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

Scenario 2: seelog as a 'receiver'

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.

Scenario 3: Custom receivers inside seelog

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.

Scenario 3.1: Custom receivers without context/with fixed context

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"/>
Clone this wiki locally