github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/operator/builtin/input/journald.go (about) 1 // +build linux 2 3 package input 4 5 import ( 6 "bufio" 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "os/exec" 12 "strconv" 13 "sync" 14 "time" 15 16 jsoniter "github.com/json-iterator/go" 17 "github.com/observiq/carbon/entry" 18 "github.com/observiq/carbon/operator" 19 "github.com/observiq/carbon/operator/helper" 20 "go.uber.org/zap" 21 ) 22 23 func init() { 24 operator.Register("journald_input", func() operator.Builder { return NewJournaldInputConfig("") }) 25 } 26 27 func NewJournaldInputConfig(operatorID string) *JournaldInputConfig { 28 return &JournaldInputConfig{ 29 InputConfig: helper.NewInputConfig(operatorID, "journald_input"), 30 StartAt: "end", 31 } 32 } 33 34 // JournaldInputConfig is the configuration of a journald input operator 35 type JournaldInputConfig struct { 36 helper.InputConfig `yaml:",inline"` 37 38 Directory *string `json:"directory,omitempty" yaml:"directory,omitempty"` 39 Files []string `json:"files,omitempty" yaml:"files,omitempty"` 40 StartAt string `json:"start_at,omitempty" yaml:"start_at,omitempty"` 41 } 42 43 // Build will build a journald input operator from the supplied configuration 44 func (c JournaldInputConfig) Build(buildContext operator.BuildContext) (operator.Operator, error) { 45 inputOperator, err := c.InputConfig.Build(buildContext) 46 if err != nil { 47 return nil, err 48 } 49 50 args := make([]string, 0, 10) 51 52 // Export logs in UTC time 53 args = append(args, "--utc") 54 55 // Export logs as JSON 56 args = append(args, "--output=json") 57 58 // Continue watching logs until cancelled 59 args = append(args, "--follow") 60 61 switch c.StartAt { 62 case "end": 63 case "beginning": 64 args = append(args, "--no-tail") 65 default: 66 return nil, fmt.Errorf("invalid value '%s' for parameter 'start_at'", c.StartAt) 67 } 68 69 switch { 70 case c.Directory != nil: 71 args = append(args, "--directory", *c.Directory) 72 case len(c.Files) > 0: 73 for _, file := range c.Files { 74 args = append(args, "--file", file) 75 } 76 } 77 78 journaldInput := &JournaldInput{ 79 InputOperator: inputOperator, 80 persist: helper.NewScopedDBPersister(buildContext.Database, c.ID()), 81 newCmd: func(ctx context.Context, cursor []byte) cmd { 82 if cursor != nil { 83 args = append(args, "--after-cursor", string(cursor)) 84 } 85 return exec.CommandContext(ctx, "journalctl", args...) 86 }, 87 json: jsoniter.ConfigFastest, 88 } 89 return journaldInput, nil 90 } 91 92 // JournaldInput is an operator that process logs using journald 93 type JournaldInput struct { 94 helper.InputOperator 95 96 newCmd func(ctx context.Context, cursor []byte) cmd 97 98 persist helper.Persister 99 json jsoniter.API 100 cancel context.CancelFunc 101 wg *sync.WaitGroup 102 } 103 104 type cmd interface { 105 StdoutPipe() (io.ReadCloser, error) 106 Start() error 107 } 108 109 var lastReadCursorKey = "lastReadCursor" 110 111 // Start will start generating log entries. 112 func (operator *JournaldInput) Start() error { 113 ctx, cancel := context.WithCancel(context.Background()) 114 operator.cancel = cancel 115 operator.wg = &sync.WaitGroup{} 116 117 err := operator.persist.Load() 118 if err != nil { 119 return err 120 } 121 122 // Start from a cursor if there is a saved offset 123 cursor := operator.persist.Get(lastReadCursorKey) 124 125 // Start journalctl 126 cmd := operator.newCmd(ctx, cursor) 127 stdout, err := cmd.StdoutPipe() 128 if err != nil { 129 return fmt.Errorf("failed to get journalctl stdout: %s", err) 130 } 131 err = cmd.Start() 132 if err != nil { 133 return fmt.Errorf("start journalctl: %s", err) 134 } 135 136 // Start a goroutine to periodically flush the offsets 137 operator.wg.Add(1) 138 go func() { 139 defer operator.wg.Done() 140 for { 141 select { 142 case <-ctx.Done(): 143 return 144 case <-time.After(time.Second): 145 operator.syncOffsets() 146 } 147 } 148 }() 149 150 // Start the reader goroutine 151 operator.wg.Add(1) 152 go func() { 153 defer operator.wg.Done() 154 defer operator.syncOffsets() 155 156 stdoutBuf := bufio.NewReader(stdout) 157 158 for { 159 line, err := stdoutBuf.ReadBytes('\n') 160 if err != nil { 161 if err != io.EOF { 162 operator.Errorw("Received error reading from journalctl stdout", zap.Error(err)) 163 } 164 return 165 } 166 167 entry, cursor, err := operator.parseJournalEntry(line) 168 if err != nil { 169 operator.Warnw("Failed to parse journal entry", zap.Error(err)) 170 continue 171 } 172 operator.persist.Set(lastReadCursorKey, []byte(cursor)) 173 operator.Write(ctx, entry) 174 } 175 }() 176 177 return nil 178 } 179 180 func (operator *JournaldInput) parseJournalEntry(line []byte) (*entry.Entry, string, error) { 181 var record map[string]interface{} 182 err := operator.json.Unmarshal(line, &record) 183 if err != nil { 184 return nil, "", err 185 } 186 187 timestamp, ok := record["__REALTIME_TIMESTAMP"] 188 if !ok { 189 return nil, "", errors.New("journald record missing __REALTIME_TIMESTAMP field") 190 } 191 192 timestampString, ok := timestamp.(string) 193 if !ok { 194 return nil, "", errors.New("journald field for timestamp is not a string") 195 } 196 197 timestampInt, err := strconv.ParseInt(timestampString, 10, 64) 198 if err != nil { 199 return nil, "", fmt.Errorf("parse timestamp: %s", err) 200 } 201 202 delete(record, "__REALTIME_TIMESTAMP") 203 204 cursor, ok := record["__CURSOR"] 205 if !ok { 206 return nil, "", errors.New("journald record missing __CURSOR field") 207 } 208 209 cursorString, ok := cursor.(string) 210 if !ok { 211 return nil, "", errors.New("journald field for cursor is not a string") 212 } 213 214 entry, err := operator.NewEntry(record) 215 if err != nil { 216 return nil, "", fmt.Errorf("failed to create entry: %s", err) 217 } 218 219 entry.Timestamp = time.Unix(0, timestampInt*1000) // in microseconds 220 return entry, cursorString, nil 221 } 222 223 func (operator *JournaldInput) syncOffsets() { 224 err := operator.persist.Sync() 225 if err != nil { 226 operator.Errorw("Failed to sync offsets", zap.Error(err)) 227 } 228 } 229 230 // Stop will stop generating logs. 231 func (operator *JournaldInput) Stop() error { 232 operator.cancel() 233 operator.wg.Wait() 234 return nil 235 }