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  }