os_release_unix.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. //go:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
  15. // +build aix dragonfly freebsd linux netbsd openbsd solaris zos
  16. package resource // import "go.opentelemetry.io/otel/sdk/resource"
  17. import (
  18. "bufio"
  19. "fmt"
  20. "io"
  21. "os"
  22. "strings"
  23. )
  24. // osRelease builds a string describing the operating system release based on the
  25. // properties of the os-release file. If no os-release file is found, or if the
  26. // required properties to build the release description string are missing, an empty
  27. // string is returned instead. For more information about os-release files, see:
  28. // https://www.freedesktop.org/software/systemd/man/os-release.html
  29. func osRelease() string {
  30. file, err := getOSReleaseFile()
  31. if err != nil {
  32. return ""
  33. }
  34. defer file.Close()
  35. values := parseOSReleaseFile(file)
  36. return buildOSRelease(values)
  37. }
  38. // getOSReleaseFile returns a *os.File pointing to one of the well-known os-release
  39. // files, according to their order of preference. If no file can be opened, it
  40. // returns an error.
  41. func getOSReleaseFile() (*os.File, error) {
  42. return getFirstAvailableFile([]string{"/etc/os-release", "/usr/lib/os-release"})
  43. }
  44. // parseOSReleaseFile process the file pointed by `file` as an os-release file and
  45. // returns a map with the key-values contained in it. Empty lines or lines starting
  46. // with a '#' character are ignored, as well as lines with the missing key=value
  47. // separator. Values are unquoted and unescaped.
  48. func parseOSReleaseFile(file io.Reader) map[string]string {
  49. values := make(map[string]string)
  50. scanner := bufio.NewScanner(file)
  51. for scanner.Scan() {
  52. line := scanner.Text()
  53. if skip(line) {
  54. continue
  55. }
  56. key, value, ok := parse(line)
  57. if ok {
  58. values[key] = value
  59. }
  60. }
  61. return values
  62. }
  63. // skip returns true if the line is blank or starts with a '#' character, and
  64. // therefore should be skipped from processing.
  65. func skip(line string) bool {
  66. line = strings.TrimSpace(line)
  67. return len(line) == 0 || strings.HasPrefix(line, "#")
  68. }
  69. // parse attempts to split the provided line on the first '=' character, and then
  70. // sanitize each side of the split before returning them as a key-value pair.
  71. func parse(line string) (string, string, bool) {
  72. k, v, found := strings.Cut(line, "=")
  73. if !found || len(k) == 0 {
  74. return "", "", false
  75. }
  76. key := strings.TrimSpace(k)
  77. value := unescape(unquote(strings.TrimSpace(v)))
  78. return key, value, true
  79. }
  80. // unquote checks whether the string `s` is quoted with double or single quotes
  81. // and, if so, returns a version of the string without them. Otherwise it returns
  82. // the provided string unchanged.
  83. func unquote(s string) string {
  84. if len(s) < 2 {
  85. return s
  86. }
  87. if (s[0] == '"' || s[0] == '\'') && s[0] == s[len(s)-1] {
  88. return s[1 : len(s)-1]
  89. }
  90. return s
  91. }
  92. // unescape removes the `\` prefix from some characters that are expected
  93. // to have it added in front of them for escaping purposes.
  94. func unescape(s string) string {
  95. return strings.NewReplacer(
  96. `\$`, `$`,
  97. `\"`, `"`,
  98. `\'`, `'`,
  99. `\\`, `\`,
  100. "\\`", "`",
  101. ).Replace(s)
  102. }
  103. // buildOSRelease builds a string describing the OS release based on the properties
  104. // available on the provided map. It favors a combination of the `NAME` and `VERSION`
  105. // properties as first option (falling back to `VERSION_ID` if `VERSION` isn't
  106. // found), and using `PRETTY_NAME` alone if some of the previous are not present. If
  107. // none of these properties are found, it returns an empty string.
  108. //
  109. // The rationale behind not using `PRETTY_NAME` as first choice was that, for some
  110. // Linux distributions, it doesn't include the same detail that can be found on the
  111. // individual `NAME` and `VERSION` properties, and combining `PRETTY_NAME` with
  112. // other properties can produce "pretty" redundant strings in some cases.
  113. func buildOSRelease(values map[string]string) string {
  114. var osRelease string
  115. name := values["NAME"]
  116. version := values["VERSION"]
  117. if version == "" {
  118. version = values["VERSION_ID"]
  119. }
  120. if name != "" && version != "" {
  121. osRelease = fmt.Sprintf("%s %s", name, version)
  122. } else {
  123. osRelease = values["PRETTY_NAME"]
  124. }
  125. return osRelease
  126. }