plugins/input/journal/unit.go (210 lines of code) (raw):

//go:build linux // +build linux // Copyright 2017 Marcus Heese // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This code based on logic from journalctl unit filter. i.e. journalctl -u in // the systemd source code. // See: https://github.com/systemd/systemd/blob/master/src/journal/journalctl.c#L1410 // and https://github.com/systemd/systemd/blob/master/src/basic/unit-name.c package journal import ( "errors" "fmt" "path" "path/filepath" "strconv" "strings" "github.com/danwakefield/fnmatch" // port of c function fnmatch to pure go ) const ( unitNameMax = 256 globChars = "*?[" uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" lowercaseLetters = "abcdefghijklmnopqrstuvwxyz" letters = lowercaseLetters + uppercaseLetters digits = "0123456789" validChars = digits + letters + ":-_.\\" validCharsWithAt = "@" + validChars validCharsGlob = validCharsWithAt + "[]!-*?" ) var systemUnits = []string{ "_SYSTEMD_UNIT", "COREDUMP_UNIT", "UNIT", "OBJECT_SYSTEMD_UNIT", "_SYSTEMD_SLICE", } var unitTypes = []string{ ".service", ".socket", ".target", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope", } // Add units to monitor func (sj *ServiceJournal) addUnits() error { var patterns []string // add specific units to monitor if any for _, unit := range sj.Units { unit, err := unitNameMangle(unit, ".service") if err != nil { return fmt.Errorf("Filtering unit %s failed: %v", unit, err) } if stringIsGlob(unit) { patterns = append(patterns, unit) } else if err = sj.addMatchesForUnit(unit); err != nil { return fmt.Errorf("Filtering unit %s failed: %v", unit, err) } } // Now add glob pattern matches if/any if len(patterns) > 0 { var units []string units = sj.getPossibleUnits(systemUnits, patterns) for _, unit := range units { if err := sj.addMatchesForUnit(unit); err != nil { return fmt.Errorf("Filtering unit %s failed: %v", unit, err) } } } return nil } // See: https://github.com/systemd/systemd/blob/master/src/shared/logs-show.c#L1114 func (sj *ServiceJournal) addMatchesForUnit(unit string) error { // Wrap AddMatch/AddDisjunction with function literal to avoid repeated checks against err. var err error AddMatch := func(s string) { if err == nil { err = sj.journal.AddMatch(s) } } AddDisjunction := func() { if err == nil { err = sj.journal.AddDisjunction() } } // Look for messages from the service itself AddMatch("_SYSTEMD_UNIT=" + unit) // Look for coredumps of the service AddDisjunction() AddMatch("MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1") AddMatch("_UID=0") AddMatch("COREDUMP_UNIT=" + unit) // Look for messages from PID 1 about this service AddDisjunction() AddMatch("_PID=1") AddMatch("UNIT=" + unit) // Look for messages from authorized daemons about this service AddDisjunction() AddMatch("_UID=0") AddMatch("OBJECT_SYSTEMD_UNIT=" + unit) // Show all messages belonging to a slice if err == nil && strings.HasSuffix(unit, ".slice") { AddDisjunction() AddMatch("_SYSTEMD_SLICE=" + unit) } AddDisjunction() return err } // Convert a string to a unit name. /dev/blah is converted to dev-blah.device, // /blah/blah is converted to blah-blah.mount, anything else is left alone, // except that "suffix" is appended if a valid unit suffix is not present. // If allowGlobs, globs characters are preserved. Otherwise, they are escaped. func unitNameMangle(name, suffix string) (string, error) { // Can't be empty or begin with a dot if len(name) == 0 || name[0] == '.' { return "", errors.New("unit name can't be empty or begin with a dot") } if !unitSuffixIsValid(suffix) { return "", errors.New("unit name has an invalid suffix") } // already a fully valid unit name? if unitNameIsValid(name) { return name, nil } // Already a fully valid globbing expression? If so, no mangling is necessary either... if stringIsGlob(name) && inCharset(name, validCharsGlob) { return name, nil } if isDevicePath(name) { // chop off path and put .device on the end return path.Base(filepath.Clean(name)) + "device", nil } if pathIsAbsolute(name) { // chop path and put .mount on the end return path.Base(filepath.Clean(name)) + ".mount", nil } name = doEscapeMangle(name) // Append a suffix if it doesn't have any, but only if this is not a glob, // so that we can allow "foo.*" as a valid glob. if !stringIsGlob(name) && !strings.ContainsAny(name, ".") { return name + suffix, nil } return name, nil } // Mangle the unit name. func doEscapeMangle(name string) string { var mangled string for _, r := range name { if r == '/' { mangled += "-" } else if !strings.ContainsRune(validChars, r) { mangled += "\\x" + strconv.FormatInt(int64(r), 16) } else { mangled += string(r) } } return mangled } // Check if this is a valid systemd unit name func unitNameIsValid(name string) bool { if len(name) >= unitNameMax { return false } dot := strings.Index(name, ".") // Must have a dot (i.e. suffix) if dot == -1 { return false } suffix := name[dot:] // Must end with a valid suffix if !unitSuffixIsValid(suffix) { return false } // name must only consist of characters from validChars + "@" if !inCharset(name, validCharsWithAt) { return false } at := strings.Index(name, "@") // Can't start with '@' if at == 0 { return false } // Plain unit (not a template or instance) or a template or instance if at == -1 || at > 0 && dot >= at+1 { return true } return false } func (sj *ServiceJournal) getPossibleUnits(fields, patterns []string) []string { var found []string var possibles []string for _, field := range fields { var vals, err = sj.journal.GetUniqueValues(field) if err != nil { continue } // Split at '=' and check against all patterns (actually GetUniqueValues does the '=' split for us) possibles = append(possibles, vals...) } // filter whole possibles list against patterns and append matches to found list for _, possible := range possibles { for _, pattern := range patterns { if fnmatch.Match(pattern, possible, fnmatch.FNM_NOESCAPE) { found = append(found, possible) break } } } return found } // Check for valid unit name suffix func unitSuffixIsValid(name string) bool { if len(name) == 0 { return false } if name[0] != '.' { return false } // Unit type from string for _, unit := range unitTypes { if strings.HasSuffix(name, unit) { return true } } return false } func inCharset(s, charset string) bool { for _, char := range s { if !strings.Contains(charset, string(char)) { return false } } return true } // Returns true on paths that refer to a device, either in sysfs or in /dev func isDevicePath(path string) bool { return strings.HasPrefix(path, "/dev/") || strings.HasPrefix(path, "/sys/") } // Absolute paths begin with a slash func pathIsAbsolute(path string) bool { return path[0] == '/' } // Return true if the provided string contains any glob chars func stringIsGlob(name string) bool { return strings.ContainsAny(name, globChars) }