// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package loggo import ( "fmt" "sort" "strings" "sync" ) // Context produces loggers for a hierarchy of modules. The context holds // a collection of hierarchical loggers and their writers. type Context struct { root *module // Perhaps have one mutex? // All `modules` variables are managed by the one mutex. modulesMutex sync.Mutex modules map[string]*module modulesLabelConfig map[string]Level writersMutex sync.Mutex writers map[string]Writer // writeMuxtex is used to serialise write operations. writeMutex sync.Mutex } // NewLoggers returns a new Context with no writers set. // If the root level is UNSPECIFIED, WARNING is used. func NewContext(rootLevel Level) *Context { if rootLevel < TRACE || rootLevel > CRITICAL { rootLevel = WARNING } context := &Context{ modules: make(map[string]*module), modulesLabelConfig: make(map[string]Level), writers: make(map[string]Writer), } context.root = &module{ level: rootLevel, context: context, } context.root.parent = context.root context.modules[""] = context.root return context } // GetLogger returns a Logger for the given module name, creating it and // its parents if necessary. func (c *Context) GetLogger(name string, labels ...string) Logger { name = strings.TrimSpace(strings.ToLower(name)) c.modulesMutex.Lock() defer c.modulesMutex.Unlock() return Logger{ impl: c.getLoggerModule(name, labels), } } // GetAllLoggerLabels returns all the logger labels for a given context. The // names are unique and sorted before returned, to improve consistency. func (c *Context) GetAllLoggerLabels() []string { c.modulesMutex.Lock() defer c.modulesMutex.Unlock() names := make(map[string]struct{}) for _, module := range c.modules { for k, v := range module.labelsLookup { names[k] = v } } labels := make([]string, 0, len(names)) for name := range names { labels = append(labels, name) } sort.Strings(labels) return labels } func (c *Context) getLoggerModule(name string, labels []string) *module { if name == rootString { name = "" } impl, found := c.modules[name] if found { return impl } var parentName string if i := strings.LastIndex(name, "."); i >= 0 { parentName = name[0:i] } // Labels don't apply to the parent, otherwise would have all labels. // Selection of the label would give you all loggers again, which isn't what // you want. parent := c.getLoggerModule(parentName, nil) // Ensure that we create a new logger module for the name, that includes the // label. level := UNSPECIFIED labelMap := make(map[string]struct{}) for _, label := range labels { labelMap[label] = struct{}{} // First label wins when setting the logger label from the config label // level cache. If there are no label configs, then fallback to // UNSPECIFIED and inherit the level correctly. if configLevel, ok := c.modulesLabelConfig[label]; ok && level == UNSPECIFIED { level = configLevel } } impl = &module{ name: name, level: level, parent: parent, context: c, labels: labels, labelsLookup: labelMap, } c.modules[name] = impl return impl } // getLoggerModulesByLabel returns modules that have the associated label. func (c *Context) getLoggerModulesByLabel(label string) []*module { var modules []*module for _, mod := range c.modules { if mod.labels == nil { continue } if _, ok := mod.labelsLookup[label]; ok { modules = append(modules, mod) } } return modules } // Config returns the current configuration of the Loggers. Loggers // with UNSPECIFIED level will not be included. func (c *Context) Config() Config { result := make(Config) c.modulesMutex.Lock() defer c.modulesMutex.Unlock() for name, module := range c.modules { if module.level != UNSPECIFIED { result[name] = module.level } } return result } // CompleteConfig returns all the loggers and their defined levels, // even if that level is UNSPECIFIED. func (c *Context) CompleteConfig() Config { result := make(Config) c.modulesMutex.Lock() defer c.modulesMutex.Unlock() for name, module := range c.modules { result[name] = module.level } return result } // ApplyConfig configures the logging modules according to the provided config. func (c *Context) ApplyConfig(config Config) { c.modulesMutex.Lock() defer c.modulesMutex.Unlock() for name, level := range config { label := extractConfigLabel(name) if label == "" { module := c.getLoggerModule(name, nil) module.setLevel(level) continue } // Ensure that we save the config for lazy loggers to pick up correctly. c.modulesLabelConfig[label] = level // Config contains a named label, use that for selecting the loggers. modules := c.getLoggerModulesByLabel(label) for _, module := range modules { module.setLevel(level) } } } // ResetLoggerLevels iterates through the known logging modules and sets the // levels of all to UNSPECIFIED, except for which is set to WARNING. func (c *Context) ResetLoggerLevels() { c.modulesMutex.Lock() defer c.modulesMutex.Unlock() // Setting the root module to UNSPECIFIED will set it to WARNING. for _, module := range c.modules { module.setLevel(UNSPECIFIED) } // We can safely just wipe everything here. c.modulesLabelConfig = make(map[string]Level) } func (c *Context) write(entry Entry) { c.writeMutex.Lock() defer c.writeMutex.Unlock() for _, writer := range c.getWriters() { writer.Write(entry) } } func (c *Context) getWriters() []Writer { c.writersMutex.Lock() defer c.writersMutex.Unlock() var result []Writer for _, writer := range c.writers { result = append(result, writer) } return result } // AddWriter adds a writer to the list to be called for each logging call. // The name cannot be empty, and the writer cannot be nil. If an existing // writer exists with the specified name, an error is returned. func (c *Context) AddWriter(name string, writer Writer) error { if name == "" { return fmt.Errorf("name cannot be empty") } if writer == nil { return fmt.Errorf("writer cannot be nil") } c.writersMutex.Lock() defer c.writersMutex.Unlock() if _, found := c.writers[name]; found { return fmt.Errorf("context already has a writer named %q", name) } c.writers[name] = writer return nil } // Writer returns the named writer if one exists. // If there is not a writer with the specified name, nil is returned. func (c *Context) Writer(name string) Writer { c.writersMutex.Lock() defer c.writersMutex.Unlock() return c.writers[name] } // RemoveWriter remotes the specified writer. If a writer is not found with // the specified name an error is returned. The writer that was removed is also // returned. func (c *Context) RemoveWriter(name string) (Writer, error) { c.writersMutex.Lock() defer c.writersMutex.Unlock() reg, found := c.writers[name] if !found { return nil, fmt.Errorf("context has no writer named %q", name) } delete(c.writers, name) return reg, nil } // ReplaceWriter is a convenience method that does the equivalent of RemoveWriter // followed by AddWriter with the same name. The replaced writer is returned. func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) { if name == "" { return nil, fmt.Errorf("name cannot be empty") } if writer == nil { return nil, fmt.Errorf("writer cannot be nil") } c.writersMutex.Lock() defer c.writersMutex.Unlock() reg, found := c.writers[name] if !found { return nil, fmt.Errorf("context has no writer named %q", name) } oldWriter := reg c.writers[name] = writer return oldWriter, nil } // ResetWriters is generally only used in testing and removes all the writers. func (c *Context) ResetWriters() { c.writersMutex.Lock() defer c.writersMutex.Unlock() c.writers = make(map[string]Writer) } // ConfigureLoggers configures loggers according to the given string // specification, which specifies a set of modules and their associated // logging levels. Loggers are colon- or semicolon-separated; each // module is specified as =. White space outside of // module names and levels is ignored. The root module is specified // with the name "". // // An example specification: // `=ERROR; foo.bar=WARNING` func (c *Context) ConfigureLoggers(specification string) error { config, err := ParseConfigString(specification) if err != nil { return err } c.ApplyConfig(config) return nil }