hook.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package log
  2. import (
  3. "bytes"
  4. "reflect"
  5. "time"
  6. "github.com/Microsoft/hcsshim/internal/logfields"
  7. "github.com/sirupsen/logrus"
  8. "go.opencensus.io/trace"
  9. )
  10. const nullString = "null"
  11. // Hook intercepts and formats a [logrus.Entry] before it logged.
  12. //
  13. // The shim either outputs the logs through an ETW hook, discarding the (formatted) output
  14. // or logs output to a pipe for logging binaries to consume.
  15. // The Linux GCS outputs logrus entries over stdout, which is then consumed and re-output
  16. // by the shim.
  17. type Hook struct {
  18. // EncodeAsJSON formats structs, maps, arrays, slices, and [bytes.Buffer] as JSON.
  19. // Variables of [bytes.Buffer] will be converted to []byte.
  20. //
  21. // Default is false.
  22. EncodeAsJSON bool
  23. // FormatTime specifies the format for [time.Time] variables.
  24. // An empty string disables formatting.
  25. // When disabled, the fall back will the JSON encoding, if enabled.
  26. //
  27. // Default is [TimeFormat].
  28. TimeFormat string
  29. // Duration format converts a [time.Duration] fields to an appropriate encoding.
  30. // nil disables formatting.
  31. // When disabled, the fall back will the JSON encoding, if enabled.
  32. //
  33. // Default is [DurationFormatString], which appends a duration unit after the value.
  34. DurationFormat DurationFormat
  35. // AddSpanContext adds [logfields.TraceID] and [logfields.SpanID] fields to
  36. // the entry from the span context stored in [logrus.Entry.Context], if it exists.
  37. AddSpanContext bool
  38. }
  39. var _ logrus.Hook = &Hook{}
  40. func NewHook() *Hook {
  41. return &Hook{
  42. TimeFormat: TimeFormat,
  43. DurationFormat: DurationFormatString,
  44. AddSpanContext: true,
  45. }
  46. }
  47. func (h *Hook) Levels() []logrus.Level {
  48. return logrus.AllLevels
  49. }
  50. func (h *Hook) Fire(e *logrus.Entry) (err error) {
  51. // JSON encode, if necessary, then add span information
  52. h.encode(e)
  53. h.addSpanContext(e)
  54. return nil
  55. }
  56. // encode loops through all the fields in the [logrus.Entry] and encodes them according to
  57. // the settings in [Hook].
  58. // If [Hook.TimeFormat] is non-empty, it will be passed to [time.Time.Format] for
  59. // fields of type [time.Time].
  60. //
  61. // If [Hook.EncodeAsJSON] is true, then fields that are not numeric, boolean, strings, or
  62. // errors will be encoded via a [json.Marshal] (with HTML escaping disabled).
  63. // Chanel- and function-typed fields, as well as unsafe pointers are left alone and not encoded.
  64. //
  65. // If [Hook.TimeFormat] and [Hook.DurationFormat] are empty and [Hook.EncodeAsJSON] is false,
  66. // then this is a no-op.
  67. func (h *Hook) encode(e *logrus.Entry) {
  68. d := e.Data
  69. formatTime := h.TimeFormat != ""
  70. formatDuration := h.DurationFormat != nil
  71. if !(h.EncodeAsJSON || formatTime || formatDuration) {
  72. return
  73. }
  74. for k, v := range d {
  75. // encode types with dedicated formatting options first
  76. if vv, ok := v.(time.Time); formatTime && ok {
  77. d[k] = vv.Format(h.TimeFormat)
  78. continue
  79. }
  80. if vv, ok := v.(time.Duration); formatDuration && ok {
  81. d[k] = h.DurationFormat(vv)
  82. continue
  83. }
  84. // general case JSON encoding
  85. if !h.EncodeAsJSON {
  86. continue
  87. }
  88. switch vv := v.(type) {
  89. // built in types
  90. // "json" marshals errors as "{}", so leave alone here
  91. case bool, string, error, uintptr,
  92. int8, int16, int32, int64, int,
  93. uint8, uint32, uint64, uint,
  94. float32, float64:
  95. continue
  96. // Rather than setting d[k] = vv.String(), JSON encode []byte value, since it
  97. // may be a binary payload and not representable as a string.
  98. // `case bytes.Buffer,*bytes.Buffer:` resolves `vv` to `interface{}`,
  99. // so cannot use `vv.Bytes`.
  100. // Could move to below the `reflect.Indirect()` call below, but
  101. // that would require additional typematching and dereferencing.
  102. // Easier to keep these duplicate branches here.
  103. case bytes.Buffer:
  104. v = vv.Bytes()
  105. case *bytes.Buffer:
  106. v = vv.Bytes()
  107. }
  108. // dereference pointer or interface variables
  109. rv := reflect.Indirect(reflect.ValueOf(v))
  110. // check if `v` is a null pointer
  111. if !rv.IsValid() {
  112. d[k] = nullString
  113. continue
  114. }
  115. switch rv.Kind() {
  116. case reflect.Map, reflect.Struct, reflect.Array, reflect.Slice:
  117. default:
  118. // Bool, [U]?Int*, Float*, Complex*, Uintptr, String: encoded as normal
  119. // Chan, Func: not supported by json
  120. // Interface, Pointer: dereferenced above
  121. // UnsafePointer: not supported by json, not safe to de-reference; leave alone
  122. continue
  123. }
  124. b, err := encode(v)
  125. if err != nil {
  126. // Errors are written to stderr (ie, to `panic.log`) and stops the remaining
  127. // hooks (ie, exporting to ETW) from firing. So add encoding errors to
  128. // the entry data to be written out, but keep on processing.
  129. d[k+"-"+logrus.ErrorKey] = err.Error()
  130. // keep the original `v` as the value,
  131. continue
  132. }
  133. d[k] = string(b)
  134. }
  135. }
  136. func (h *Hook) addSpanContext(e *logrus.Entry) {
  137. ctx := e.Context
  138. if !h.AddSpanContext || ctx == nil {
  139. return
  140. }
  141. span := trace.FromContext(ctx)
  142. if span == nil {
  143. return
  144. }
  145. sctx := span.SpanContext()
  146. e.Data[logfields.TraceID] = sctx.TraceID.String()
  147. e.Data[logfields.SpanID] = sctx.SpanID.String()
  148. }