os_release_unix.go (4733B)
1 // Copyright The OpenTelemetry Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos 16 // +build aix dragonfly freebsd linux netbsd openbsd solaris zos 17 18 package resource // import "go.opentelemetry.io/otel/sdk/resource" 19 20 import ( 21 "bufio" 22 "fmt" 23 "io" 24 "os" 25 "strings" 26 ) 27 28 // osRelease builds a string describing the operating system release based on the 29 // properties of the os-release file. If no os-release file is found, or if the 30 // required properties to build the release description string are missing, an empty 31 // string is returned instead. For more information about os-release files, see: 32 // https://www.freedesktop.org/software/systemd/man/os-release.html 33 func osRelease() string { 34 file, err := getOSReleaseFile() 35 if err != nil { 36 return "" 37 } 38 39 defer file.Close() 40 41 values := parseOSReleaseFile(file) 42 43 return buildOSRelease(values) 44 } 45 46 // getOSReleaseFile returns a *os.File pointing to one of the well-known os-release 47 // files, according to their order of preference. If no file can be opened, it 48 // returns an error. 49 func getOSReleaseFile() (*os.File, error) { 50 return getFirstAvailableFile([]string{"/etc/os-release", "/usr/lib/os-release"}) 51 } 52 53 // parseOSReleaseFile process the file pointed by `file` as an os-release file and 54 // returns a map with the key-values contained in it. Empty lines or lines starting 55 // with a '#' character are ignored, as well as lines with the missing key=value 56 // separator. Values are unquoted and unescaped. 57 func parseOSReleaseFile(file io.Reader) map[string]string { 58 values := make(map[string]string) 59 scanner := bufio.NewScanner(file) 60 61 for scanner.Scan() { 62 line := scanner.Text() 63 64 if skip(line) { 65 continue 66 } 67 68 key, value, ok := parse(line) 69 if ok { 70 values[key] = value 71 } 72 } 73 74 return values 75 } 76 77 // skip returns true if the line is blank or starts with a '#' character, and 78 // therefore should be skipped from processing. 79 func skip(line string) bool { 80 line = strings.TrimSpace(line) 81 82 return len(line) == 0 || strings.HasPrefix(line, "#") 83 } 84 85 // parse attempts to split the provided line on the first '=' character, and then 86 // sanitize each side of the split before returning them as a key-value pair. 87 func parse(line string) (string, string, bool) { 88 k, v, found := strings.Cut(line, "=") 89 90 if !found || len(k) == 0 { 91 return "", "", false 92 } 93 94 key := strings.TrimSpace(k) 95 value := unescape(unquote(strings.TrimSpace(v))) 96 97 return key, value, true 98 } 99 100 // unquote checks whether the string `s` is quoted with double or single quotes 101 // and, if so, returns a version of the string without them. Otherwise it returns 102 // the provided string unchanged. 103 func unquote(s string) string { 104 if len(s) < 2 { 105 return s 106 } 107 108 if (s[0] == '"' || s[0] == '\'') && s[0] == s[len(s)-1] { 109 return s[1 : len(s)-1] 110 } 111 112 return s 113 } 114 115 // unescape removes the `\` prefix from some characters that are expected 116 // to have it added in front of them for escaping purposes. 117 func unescape(s string) string { 118 return strings.NewReplacer( 119 `\$`, `$`, 120 `\"`, `"`, 121 `\'`, `'`, 122 `\\`, `\`, 123 "\\`", "`", 124 ).Replace(s) 125 } 126 127 // buildOSRelease builds a string describing the OS release based on the properties 128 // available on the provided map. It favors a combination of the `NAME` and `VERSION` 129 // properties as first option (falling back to `VERSION_ID` if `VERSION` isn't 130 // found), and using `PRETTY_NAME` alone if some of the previous are not present. If 131 // none of these properties are found, it returns an empty string. 132 // 133 // The rationale behind not using `PRETTY_NAME` as first choice was that, for some 134 // Linux distributions, it doesn't include the same detail that can be found on the 135 // individual `NAME` and `VERSION` properties, and combining `PRETTY_NAME` with 136 // other properties can produce "pretty" redundant strings in some cases. 137 func buildOSRelease(values map[string]string) string { 138 var osRelease string 139 140 name := values["NAME"] 141 version := values["VERSION"] 142 143 if version == "" { 144 version = values["VERSION_ID"] 145 } 146 147 if name != "" && version != "" { 148 osRelease = fmt.Sprintf("%s %s", name, version) 149 } else { 150 osRelease = values["PRETTY_NAME"] 151 } 152 153 return osRelease 154 }