Different packages with different config props - Functional option - go

I have an application which needs configuration and I’ve created a configuration struct and I’m entering the configuration as a parameter to the function. The problem is that the configuration struct becomes bigger (like monolith) and bigger and I move the config to different functions in my app and which doesn’t need all the fields, just few of them. My question is if there is better approach to implement it in Go.
After struggling to find good way I’ve found this article (which a bit old but hopefully still relevant) and I wonder how and if I can use it to solve my problem.
Functional options instead of config struct
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
I need to inject some configuration properties to my application in
For example for function run (which is entry point ) I need to inject the log level and some other env variable like port host
For function build I need to “inject” the build flavor and build type etc.
Any example for my content will be very helpful
How to structure it in the code ?
How to implement it?
update
I need some E2E example how can I use the functional approach for different configs in the same package and other packages

It sounds like you're looking for an alternative to passing around the same configuration monolith structure to every package and every function. There are many solutions to this problem (more than I'm going to list here), and which one is right for you requires more knowledge of your code and your goals than we have, so it's probably best if you decide. And it sounds like you're wondering whether Dave Cheney's post on functional options provides a solution and how to apply it.
If your application's configuration is static in that it's not likely to change (mutate) through different threads of execution, and you don't need to create multiple instances with different configurations in the same main, then one option is package level variables and package initialization. If you object to exported package variables, you can use unexported package variables and control access via exported functions. Say run and build are two different packages:
// package main
import(
"github.com/profilename/appname/build"
"github.com/profilename/appname/run"
)
func main() {
// do something to get configuration values
build.Initialize(buildFlavor, buildType)
// any post-build-initialize-pre-run-initialize stuff
run.Initialize(logLevel, port, host)
// other processing
build.PreBuild("title") // other build functions may rely on configuration
build.Build()
// other stuff
run.ReadFiles(f1, f2)
run.Validate(preferredBackupPort) // port availability, chance to log.Fatal out
run.Run()
// cleanup
}
// package run
var Host string
var LogLevel, Port int
init() {
Host = `localhost`
Port = 8888
Loglevel = 1
}
func Initialize(logLevel, port int, host string) {
// validation, panic on failure
LogLevel = logLevel
Host = host
Port = port
}
func Run() {
// do something with LogLevel, Host, Port
}
But that doesn't solve the problem addressed in Dave Cheney's post. What if the user is running this without host, port, or buildType (or other configuration variables), because he doesn't need those features? What if the user wants to run multiple instances with different configurations?
Dave's approach is primarily intended for situations where you will not use package-level variables for configuration. Indeed, it is meant to enable several instances of a thing where each instance can have a different configuration. Your optional configuration parameters become a single variadic parameter where the type is a function that modifies a pointer to the thing being configured. For you, that could be
// package run
type Runner struct {
Port int
// rest of runner configuration
}
func NewRunner(options ...func(*Runner)) (runner *Runner, err error) {
// any setup
for _, option := range options {
err = option(runner)
if err != nil {
// do something
}
}
return runner, err
}
// package main
func main() {
// do something to get configuration values
port := func(runner *Runner) {
runner.Port = configuredPort
}
// other configuration if applicable
runner := run.NewRunner(port)
// ...
In a way, Dave's approach appears targeted at packages that will be used as very flexible libraries, and will provide application interfaces that users might wish to create several instances of. It allows for main definitions that launch multiple instances with different configurations. In that post he doesn't go into detail on how to process configuration input in the main or on a configuration package.
Note that the way the port is set in the resulting code above is not very different from this:
// package run
type Runner struct {
Port int
// rest of runner configuration
}
// package main, func main()
runner := new(run.Runner)
runner.Port = configuredPort
which is more traditional, probably easier for most developers to read and understand, and a perfectly fine approach if it suits your needs. (And you could make runner.port unexported and add a func (r *Runner) SetPort(p int) { r.port = p } method if you wanted.) It is also a design that has the potential, depending on implementation, to deal with mutating configuration, multiple threads of execution (you'll need channels or the sync package to deal with mutation there), and multiple instances.
Where the function options design Dave proposed becomes much more powerful than that approach is when you have many more statements related to the setting of the option that you want to place in main rather than in run -- those will make up the function body.
UPDATE Here's a runnable example using Dave's functional options approach, in two files. Be sure to update the import path to match wherever you put the run package.
Package run:
package run
import(
"fmt"
"log"
)
const(
DefaultPort = 8888
DefaultHost = `localhost`
DefaultLogLevel = 1
)
type Runner struct {
Port int
Host string
LogLevel int
}
func NewRunner(options ...func(*Runner) error) (runner *Runner) {
// any setup
// set defaults
runner = &Runner{DefaultPort, DefaultHost, DefaultLogLevel}
for _, option := range options {
err := option(runner)
if err != nil {
log.Fatalf("Failed to set NewRunner option: %s\n", err)
}
}
return runner
}
func (r *Runner) Run() {
fmt.Println(r)
}
func (r *Runner) String() string {
return fmt.Sprintf("Runner Configuration:\n%16s %22d\n%16s %22s\n%16s %22d",
`Port`, r.Port, `Host`, r.Host, `LogLevel`, r.LogLevel)
}
Package main:
package main
import(
"errors"
"flag"
"github.com/jrefior/run" // update this path for your filesystem
)
func main() {
// do something to get configuration values
portFlag := flag.Int("p", 0, "Override default listen port")
logLevelFlag := flag.Int("l", 0, "Override default log level")
flag.Parse()
// put your runner options here
runnerOpts := make([]func(*run.Runner) error, 0)
// with flags, we're not sure if port was set by flag, so test
if *portFlag > 0 {
runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
if *portFlag < 1024 {
return errors.New("Ports below 1024 are privileged")
}
runner.Port = *portFlag
return nil
})
}
if *logLevelFlag > 0 {
runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
if *logLevelFlag > 8 {
return errors.New("The maximum log level is 8")
}
runner.LogLevel = *logLevelFlag
return nil
})
}
// other configuration if applicable
runner := run.NewRunner(runnerOpts...)
runner.Run()
}
Example usage:
$ ./program -p 8987
Runner Configuration:
Port 8987
Host localhost
LogLevel 1

I use this to define per package Config Structs which are easier to manage and are loaded at the app start.
Define your config struct like this
type Config struct {
Conf1 package1.Configuration `group:"conf1" namespace:"conf1"`
Conf2 package2.Configuration `group:"conf2" namespace:"conf2"`
Conf3 Config3 `group:"conf3" namespace:"conf3"`
GeneralSetting string `long:"Setting" description:"setting" env:"SETTING" required:"true"`
}
type Config3 struct {
setting string
}
And use "github.com/jessevdk/go-flags" to pass either --config3.setting=stringValue cmd arguments, or ENV variables export CONFIG3_SETTING=stringValue:
type Configuration interface {}
const DefaultFlags flags.Options = flags.HelpFlag | flags.PassDoubleDash
func Parse(cfg Configuration) []string {
args, _ := flags.NewParser(cfg, DefaultFlags).Parse()
return args
}
And your main should look something like this:
func main() {
// Parse the configuration.
var cfg Config
Parse(&cfg)
service := NewService(cfg.Conf3.Setting)
}

Related

How can I separate generated code package and user code but have them accessible from one place in code

I am newer to golang, so I have some courses that I bought from udemy to help break me into the language. One of them I found very helpful for a general understanding as I took on a project in the language.
In the class that I took, all of the sql related functions were in the sqlc folder with the structure less broken out:
sqlc
generatedcode
store
One of those files is a querier that is generated by sqlc that contains an interface with all of the methods that were generated. Here is the general idea of what it currently looks like: https://github.com/techschool/simplebank/tree/master/db/sqlc
package db
import (
"context"
"github.com/google/uuid"
)
type Querier interface {
AddAccountBalance(ctx context.Context, arg AddAccountBalanceParams) (Account, error)
CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error)
...
}
var _ Querier = (*Queries)(nil)
Would it be possible to wrap both what sqlc generates AND any queries that a developer creates (dynamic queries) into a single querier? I'm also trying to have it so that the sqlc generated code is in its own folder. The structure I am aiming for is:
sql
sqlc
generatedcode
store - (wraps it all together)
dynamicsqlfiles
This should clear up what a I mean by store: https://github.com/techschool/simplebank/blob/master/db/sqlc/store.go
package db
import (
"context"
"database/sql"
"fmt"
)
// Store defines all functions to execute db queries and transactions
type Store interface {
Querier
TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)
}
// SQLStore provides all functions to execute SQL queries and transactions
type SQLStore struct {
db *sql.DB
*Queries
}
// NewStore creates a new store
func NewStore(db *sql.DB) Store {
return &SQLStore{
db: db,
Queries: New(db),
}
}
I'm trying to run everything through that store (both generated and my functions), so I can make a call similar to the CreateUser function in this file (server.store.): https://github.com/techschool/simplebank/blob/master/api/user.go
arg := db.CreateUserParams{
Username: req.Username,
HashedPassword: hashedPassword,
FullName: req.FullName,
Email: req.Email,
}
user, err := server.store.CreateUser(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "unique_violation":
ctx.JSON(http.StatusForbidden, errorResponse(err))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
I've tried creating something that houses another querier interface that embeds the generated one, then creating my own db.go that uses the generated DBTX interface but has its own Queries struct, and New function. It always gives me an error that the Queries struct I created aren't implementing the functions I made, despite having it implemented in one of the custom methods I made.
I deleted that branch, and have been clicking through the simplebank project linked above to see if I can find another way this could be done, or if I missed something. If it can't be done, that's okay. I'm just using this as a good opportunity to learn a little more about the language, and keep some code separated if possible.
UPDATE:
There were only a few pieces I had to change, but I modified the store.go to look more like:
// sdb is imported, but points to the generated Querier
// Store provides all functions to execute db queries and transactions
type Store interface {
sdb.Querier
DynamicQuerier
}
// SQLStore provides all functions to execute SQL queries and transactions
type SQLStore struct {
db *sql.DB
*sdb.Queries
*dynamicQueries
}
// NewStore creates a new Store
func NewStore(db *sql.DB) Store {
return &SQLStore{
db: db,
Queries: sdb.New(db),
dynamicQueries: New(db),
}
}
Then just created a new Querier and struct for the methods I would be creating. Gave them their own New function, and tied it together in the above. Before, I was trying to figure out a way to reuse as much of the generated code as possible, which I think was the issue.
Why I wanted the Interface:
I wanted a structure that separated the files I would be working in more from the files that I would never touch (generated). This is the new structure:
I like how the generated code put everything in the Querier interface, then checked that anything implementing it satisfied all of the function requirements. So I wanted to replicate that for the dynamic portion which I would be creating on my own.
It might be complicating it a bit more than it would 'NEED' to be, but it also provides an additional set of error checking that is nice to have. And in this case, even while maybe not necessary, it ended up being doable.
Would it be possible to wrap both what sqlc generates AND any queries that a developer creates (dynamic queries) into a single querier?
If I'm understanding your question correctly I think that you are looking for something like the below (playground):
package main
import (
"context"
"database/sql"
)
// Sample SQL C Code
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
type Queries struct {
db DBTX
}
func (q *Queries) DeleteAccount(ctx context.Context, id int64) error {
// _, err := q.db.ExecContext(ctx, deleteAccount, id)
// return err
return nil // Pretend that this always works
}
type Querier interface {
DeleteAccount(ctx context.Context, id int64) error
}
//
// Your custom "dynamic" queries
//
type myDynamicQueries struct {
db DBTX
}
func (m *myDynamicQueries) GetDynamicResult(ctx context.Context) error {
// _, err := q.db.ExecContext(ctx, deleteAccount, id)
// return err
return nil // Pretend that this always works
}
type myDynamicQuerier interface {
GetDynamicResult(ctx context.Context) error
}
// Combine things
type allDatabase struct {
*Queries // Note: You could embed this directly into myDynamicQueries instead of having a seperate struct if that is your preference
*myDynamicQueries
}
type DatabaseFunctions interface {
Querier
myDynamicQuerier
}
func main() {
// Basic example
var db DatabaseFunctions
db = getDatabase()
db.DeleteAccount(context.Background(), 0)
db.GetDynamicResult(context.Background())
}
// getDatabase - Perform whatever is needed to connect to database...
func getDatabase() allDatabase {
sqlc := &Queries{db: nil} // In reality you would use New() to do this!
myDyn := &myDynamicQueries{db: nil} // Again it's often cleaner to use a function
return allDatabase{Queries: sqlc, myDynamicQueries: myDyn}
}
The above is all in one file for simplicity but could easily pull from multiple packages e.g.
type allDatabase struct {
*generatedcode.Queries
*store.myDynamicQueries
}
If this does not answer your question then please show one of your failed attempts (so we can see where you are going wrong).
One general comment - do you really need the interface? A common recommendation is "Accept interfaces, return structs". While this may not always apply I suspect you may be introducing interfaces where they are not really necessary and this may add unnecessary complexity.
I thought that the Store, which was housing both Queriers, was tying it all together. Can you explain a little with the example above (in the question post) why it's not necessary? How does SQLStore get access to all of the Querier interface functions?
The struct SQLStore is what is "tying it all together". As per the Go spec:
Given a struct type S and a named type T, promoted methods are included in the method set of the struct as follows:
If S contains an embedded field T, the method sets of S and *S both include promoted methods with receiver T. The method set of *S also includes promoted methods with receiver *T.
If S contains an embedded field *T, the method sets of S and *S both include promoted methods with receiver T or *T.
So an object of type SQLStore:
type SQLStore struct {
db *sql.DB
*sdb.Queries
*dynamicQueries
}
var foo SQLStore // Assume that we are actually providing values for all fields
Will implement all of the methods of sdb.Queries and, also, those in dynamicQueries (you can also access the sql.DB members via foo.db.XXX). This means that you can call foo.AddAccountBalance() and foo.MyGenericQuery() (assuming that is in dynamicQueries!) etc.
The spec says "In its most basic form an interface specifies a (possibly empty) list of methods". So you can think of an interface as a list of functions that must be implemented by whatever implementation (e.g. struct) you assign to the interface (the interface itself does not implement anything directly).
This example might help you understand.
Hopefully that helps a little (as I'm not sure which aspect you don't understand I'm not really sure what to focus on).

Table driven tests for private types with public constructors

I'm trying to reduce the surface area of my API, so I made my app struct non-exported (with the lowercase name), and only exposed the New function:
package mylib
type app struct {
}
func New() *app {
return &app{}
}
But now, I want to write a table-driven test for this thing, and I can't hold a mylib.app in a struct:
package mylib_test
import (
"testing"
"mylib"
)
func TestApp(t *testing.T) {
tests := []struct {
name string
app private_type_public_new.app // This part doesn't work
}{
// ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
})
}
}
What options do I have? Should I make the app struct public (App) and leave all the fields unexported? Is there something interesting I can do with higher order functions to store instances of the New function to instantiate apps within the subtests? Something else?
What options do I have? Should I make the app struct public (App) and leave all the fields unexported?
Yes, export it as App. golang/lint (now deprecated) specifically warned about exported functions that referenced unexported types, as they are difficult for consumers of your package to work with. For example, if you assign x := mylib.New() such that x is an instance of *myapp.app, go-pls will show nothing about this variable except its type, meaning any descriptive comment you've attached to it will not appear.
See https://github.com/golang/lint/issues/210

Having a singleton pattern in Go Wire injection

I have a piece of code which is used to load configuration from file and parse it into a struct, I use this configuration quite often and hence I pass it around in the method parameters. Now I as my method parameters are increasing, I am looking at dependency injection and have settle with wire.
Now I have created a provider to load the configuration and an injector to provide the config struct. However each time I call the injection my file is read again, I want that the file is read once and the injection provided as many times as required without any additional loading.
Here is my provider:
// ProvideConfig config provider ...
func ProvideConfig() *config.FileConfig {
var cfp string
flag.StringVar(&cfp, "config", "config.json", "absolute path")
flag.Parse()
return config.Loadconfig(cfp)
}
Injector:
// GetConfig injector ...
func GetConfig() ConfigResource {
wire.Build(ProvideConfig, NewConfigResource)
return ConfigResource{}
}
Now when I call:
injection.GetConfig()
I see that ProvideConfig is called always. I can have a check in the provide config method the determine if the config is already loaded, I am not sure if there is a better way, something like a single instance loader which is built into the wire. I tried looking into the docs but could not find anything relevant.
As far as I'm aware, there's no built in way in wire to specify that a provider is a singleton / should only be called once.
This is accomplished in the usual way in Go, by using sync.Once. Your provider function can be a closure that does the expensive operation only once using sync.Once.Do. This is idiomatic in Go, and doesn't require any special provision from every library that wants to provide "single" loading.
Here's an example without wire:
type Value struct {
id int
msg string
}
type ValueProvider func() *Value
// consumer takes a function that provides a new *Value and consumes
// the *Value provided by it.
func consumer(vp ValueProvider) {
v := vp()
fmt.Printf("Consuming %+v\n", *v)
}
// MakeSingleLoader returns a ValueProvider that creates a value once using an
// expensive operation, and then keeps returning the same value.
func MakeSingleLoader() ValueProvider {
var v *Value
var once sync.Once
return func() *Value {
once.Do(func() {
v = ExpensiveOperation()
})
return v
}
}
// ExpensiveOperation emulates an expensive operation that can take a while
// to run.
func ExpensiveOperation() *Value {
return &Value{id: 1, msg: "hello"}
}
func main() {
sl := MakeSingleLoader()
consumer(sl)
consumer(sl)
consumer(sl)
}
If you're OK with the "singleton" value being a global, this code can be simplified a bit. Otherwise, it only calls ExpensiveOperation once, and keeps the value cached in a local inaccessible outside MakeSingleLoader.

Read template in init or in handler function?

I'm writing a basic server for a website. Now I face a (for me) difficult performance question. Is it better to read the template file in the init() function?
// Initialize all pages of website
func init(){
indexPageData, err := ioutil.ReadFile("./tpl/index.tpl")
check(err)
}
Or in the http.HandlerFunc?
func index(w http.ResponseWriter, req *http.Request){
indexPageData, err := ioutil.ReadFile("./tpl/index.tpl")
check(err)
indexPageTpl := template.Must(template.New("index").Parse(string(indexPageData)))
indexPageTpl.Execute(w, "test")
}
I think in the first example, after the server is started you have no need to access the disk and increase the performance of the request.
But during development I want to refresh the browser and see the new content. That can be done with the second example.
Does someone have a state-of-the-art solution? Or what is the right from the performance point of view?
Let's analyze the performance:
We name your first solution (with slight changes, see below) a and your second solution b.
One request:
a: One disk access
b: One disk access
Ten requests:
a: One disk access
b: Ten disk accesses
10 000 000 requests:
a: One disk access
b: 10 000 000 disk accesses (this is slow)
So, performance is better with your first solution. But what about your concern regarding up-to-date data? From the documentation of func (t *Template) Execute(wr io.Writer, data interface{}) error:
Execute applies a parsed template to the specified data object, writing the output to wr. If an error occurs executing the template or writing its output, execution stops, but partial results may already have been written to the output writer. A template may be executed safely in parallel.
So, what happens is this:
You read a template from disk
You parse the file into a template
You choose the data to fill in the blanks with
You Execute the template with that data, the result is written out into an io.Writer
Your data is as up-to-date as you choose it. This has nothing to do with re-reading the template from disk, or even re-parsing it. This is the whole idea behind templates: One disk access, one parse, multiple dynamic end results.
The documentation quoted above tells us another thing:
A template may be executed safely in parallel.
This is very useful, because your http.HandlerFuncs are ran in parallel, if you have multiple requests in parallel.
So, what to do now?
Read the template file once,
Parse the template once,
Execute the template for every request.
I'm not sure if you should read and parse in the init() function, because at least the Must can panic (and don't use some relative, hard coded path in there!) - I would try to do that in a more controlled environment, e.g. provide a function (like New()) to create a new instance of your server and do that stuff in there.
EDIT: I re-read your question and I might have misunderstood you:
If the template itself is still in development then yes, you would have to read it on every request to have an up-to-date result. This is more convenient than to restart the server every time you change the template. For production, the template should be fixed and only the data should change.
Sorry if I got you wrong there.
Never read and parse template files in the request handler in production, that is as bad as it can get (you should like always avoid this). During development it is ok of course.
Read this question for more details:
It takes too much time when using "template" package to generate a dynamic web page to client in golang
You could approach this in multiple ways. Here I list 4 with example implementation.
1. With a "dev mode" setting
You could have a constant or variable telling if you're running in development mode which means templates are not to be cached.
Here's an example to that:
const dev = true
var indexTmpl *template.Template
func init() {
if !dev { // Prod mode, read and cache template
indexTmpl = template.Must(template.New("index").ParseFiles(".tpl/index.tpl"))
}
}
func getIndexTmpl() *template.Template {
if dev { // Dev mode, always read fresh template
return template.Must(template.New("index").ParseFiles(".tpl/index.tpl"))
} else { // Prod mode, return cached template
return indexTmpl
}
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
getIndexTmpl().Execute(w, "test")
}
2. Specify in the request (as a param) if you want a fresh template
When you develop, you may specify an extra URL parameter indicating to read a fresh template and not use the cached one, e.g. http://localhost:8080/index?dev=true
Example implementation:
var indexTmpl *template.Template
func init() {
indexTmpl = getIndexTmpl()
}
func getIndexTmpl() *template.Template {
return template.Must(template.New("index").ParseFiles(".tpl/index.tpl"))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
t := indexTmpl
if r.FormValue("dev") != nil {
t = getIndexTmpl()
}
t.Execute(w, "test")
}
3. Decide based on host
You can also check the host name of the request URL, and if it is "localhost", you can omit the cache and use a fresh template. This requires the smallest extra code and effort. Note that you may want to accept other hosts as well e.g. "127.0.0.1" (up to you what you want to include).
Example implementation:
var indexTmpl *template.Template
func init() {
indexTmpl = getIndexTmpl()
}
func getIndexTmpl() *template.Template {
return template.Must(template.New("index").ParseFiles(".tpl/index.tpl"))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
t := indexTmpl
if r.URL.Host == "localhost" || strings.HasPrefix(r.URL.Host, "localhost:") {
t = getIndexTmpl()
}
t.Execute(w, "test")
}
4. Check template file last modified
You could also store the last modified time of the template file when it is loaded. Whenever the template is requested, you can check the last modified time of the source template file. If it has changed, you can reload it before executing it.
Example implementation:
type mytempl struct {
t *template.Template
lastmod time.Time
mutex sync.Mutex
}
var indexTmpl mytempl
func init() {
// You may want to call this in init so first request won't be slow
checkIndexTempl()
}
func checkIndexTempl() {
nm := ".tpl/index.tpl"
fi, err := os.Stat(nm)
if err != nil {
panic(err)
}
if indexTmpl.lastmod != fi.ModTime() {
// Changed, reload. Don't forget the locking!
indexTmpl.mutex.Lock()
defer indexTmpl.mutex.Unlock()
indexTmpl.t = template.Must(template.New("index").ParseFiles(nm))
indexTmpl.lastmod = fi.ModTime()
}
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
checkIndexTempl()
indexTmpl.t.Execute(w, "test")
}

Golang logrus - how to do a centralized configuration?

I am using logrus in a Go app. I believe this question is applicable to any other logging package (which doesn't offer external file based configuration) as well.
logrus provides functions to setup various configuration, e.g. SetOutput, SetLevel etc.
Like any other application I need to do logging from multiple source files/packages, it seems you need to setup these options in each file with logrus.
Is there any way to setup these options once somewhere in a central place to be shared all over the application. That way if I have to make logging level change I can do it in one place and applies to all the components of the app.
You don't need to set these options in each file with Logrus.
You can import Logrus as log:
import log "github.com/Sirupsen/logrus"
Then functions like log.SetOutput() are just functions and modify the global logger and apply to any file that includes this import.
You can create a package global log variable:
var log = logrus.New()
Then functions like log.SetOutput() are methods and modify your package global. This is awkward IMO if you have multiple packages in your program, because each of them has a different logger with different settings (but maybe that's good for some use cases). I also don't like this approach because it confuses goimports (which will want to insert log into your imports list).
Or you can create your own wrapper (which is what I do). I have my own log package with its own logger var:
var logger = logrus.New()
Then I make top-level functions to wrap Logrus:
func Info(args ...interface{}) {
logger.Info(args...)
}
func Debug(args ...interface{}) {
logger.Debug(args...)
}
This is slightly tedious, but allows me to add functions specific to my program:
func WithConn(conn net.Conn) *logrus.Entry {
var addr string = "unknown"
if conn != nil {
addr = conn.RemoteAddr().String()
}
return logger.WithField("addr", addr)
}
func WithRequest(req *http.Request) *logrus.Entry {
return logger.WithFields(RequestFields(req))
}
So I can then do things like:
log.WithConn(c).Info("Connected")
(I plan in the future to wrap logrus.Entry into my own type so that I can chain these better; currently I can't call log.WithConn(c).WithRequest(r).Error(...) because I can't add WithRequest() to logrus.Entry.)
This is the solution that I arrived at for my application that allows adding of fields to the logging context. It does have a small performance impact due to the copying of context base fields.
package logging
import (
log "github.com/Sirupsen/logrus"
)
func NewContextLogger(c log.Fields) func(f log.Fields) *log.Entry {
return func(f log.Fields) *log.Entry {
for k, v := range c {
f[k] = v
}
return log.WithFields(f)
}
}
package main
import (
"logging"
)
func main {
app.Logger = logging.NewContextLogger(log.Fields{
"module": "app",
"id": event.Id,
})
app.Logger(log.Fields{
"startTime": event.StartTime,
"endTime": event.EndTime,
"title": event.Name,
}).Info("Starting process")
}

Resources