github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/journalctl/journalctl.go (about) 1 package journalctlacquisition 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "net/url" 8 "os/exec" 9 "strings" 10 "time" 11 12 "github.com/prometheus/client_golang/prometheus" 13 log "github.com/sirupsen/logrus" 14 "gopkg.in/tomb.v2" 15 "gopkg.in/yaml.v2" 16 17 "github.com/crowdsecurity/go-cs-lib/trace" 18 19 "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" 20 "github.com/crowdsecurity/crowdsec/pkg/types" 21 ) 22 23 type JournalCtlConfiguration struct { 24 configuration.DataSourceCommonCfg `yaml:",inline"` 25 Filters []string `yaml:"journalctl_filter"` 26 } 27 28 type JournalCtlSource struct { 29 metricsLevel int 30 config JournalCtlConfiguration 31 logger *log.Entry 32 src string 33 args []string 34 } 35 36 const journalctlCmd string = "journalctl" 37 38 var ( 39 journalctlArgsOneShot = []string{} 40 journalctlArgstreaming = []string{"--follow", "-n", "0"} 41 ) 42 43 var linesRead = prometheus.NewCounterVec( 44 prometheus.CounterOpts{ 45 Name: "cs_journalctlsource_hits_total", 46 Help: "Total lines that were read.", 47 }, 48 []string{"source"}) 49 50 func readLine(scanner *bufio.Scanner, out chan string, errChan chan error) error { 51 for scanner.Scan() { 52 txt := scanner.Text() 53 out <- txt 54 } 55 if errChan != nil && scanner.Err() != nil { 56 errChan <- scanner.Err() 57 close(errChan) 58 // the error is already consumed by runJournalCtl 59 return nil //nolint:nilerr 60 } 61 if errChan != nil { 62 close(errChan) 63 } 64 return nil 65 } 66 67 func (j *JournalCtlSource) runJournalCtl(out chan types.Event, t *tomb.Tomb) error { 68 ctx, cancel := context.WithCancel(context.Background()) 69 70 cmd := exec.CommandContext(ctx, journalctlCmd, j.args...) 71 stdout, err := cmd.StdoutPipe() 72 if err != nil { 73 cancel() 74 return fmt.Errorf("could not get journalctl stdout: %s", err) 75 } 76 stderr, err := cmd.StderrPipe() 77 if err != nil { 78 cancel() 79 return fmt.Errorf("could not get journalctl stderr: %s", err) 80 } 81 82 stderrChan := make(chan string) 83 stdoutChan := make(chan string) 84 errChan := make(chan error, 1) 85 86 logger := j.logger.WithField("src", j.src) 87 88 logger.Infof("Running journalctl command: %s %s", cmd.Path, cmd.Args) 89 err = cmd.Start() 90 if err != nil { 91 cancel() 92 logger.Errorf("could not start journalctl command : %s", err) 93 return err 94 } 95 96 stdoutscanner := bufio.NewScanner(stdout) 97 98 if stdoutscanner == nil { 99 cancel() 100 cmd.Wait() 101 return fmt.Errorf("failed to create stdout scanner") 102 } 103 104 stderrScanner := bufio.NewScanner(stderr) 105 106 if stderrScanner == nil { 107 cancel() 108 cmd.Wait() 109 return fmt.Errorf("failed to create stderr scanner") 110 } 111 t.Go(func() error { 112 return readLine(stdoutscanner, stdoutChan, errChan) 113 }) 114 t.Go(func() error { 115 //looks like journalctl closes stderr quite early, so ignore its status (but not its output) 116 return readLine(stderrScanner, stderrChan, nil) 117 }) 118 119 for { 120 select { 121 case <-t.Dying(): 122 logger.Infof("journalctl datasource %s stopping", j.src) 123 cancel() 124 cmd.Wait() //avoid zombie process 125 return nil 126 case stdoutLine := <-stdoutChan: 127 l := types.Line{} 128 l.Raw = stdoutLine 129 logger.Debugf("getting one line : %s", l.Raw) 130 l.Labels = j.config.Labels 131 l.Time = time.Now().UTC() 132 l.Src = j.src 133 l.Process = true 134 l.Module = j.GetName() 135 if j.metricsLevel != configuration.METRICS_NONE { 136 linesRead.With(prometheus.Labels{"source": j.src}).Inc() 137 } 138 var evt types.Event 139 if !j.config.UseTimeMachine { 140 evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.LIVE} 141 } else { 142 evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: types.TIMEMACHINE} 143 } 144 out <- evt 145 case stderrLine := <-stderrChan: 146 logger.Warnf("Got stderr message : %s", stderrLine) 147 err := fmt.Errorf("journalctl error : %s", stderrLine) 148 t.Kill(err) 149 case errScanner, ok := <-errChan: 150 if !ok { 151 logger.Debugf("errChan is closed, quitting") 152 t.Kill(nil) 153 } 154 if errScanner != nil { 155 t.Kill(errScanner) 156 } 157 } 158 } 159 } 160 161 func (j *JournalCtlSource) GetUuid() string { 162 return j.config.UniqueId 163 } 164 165 func (j *JournalCtlSource) GetMetrics() []prometheus.Collector { 166 return []prometheus.Collector{linesRead} 167 } 168 169 func (j *JournalCtlSource) GetAggregMetrics() []prometheus.Collector { 170 return []prometheus.Collector{linesRead} 171 } 172 173 func (j *JournalCtlSource) UnmarshalConfig(yamlConfig []byte) error { 174 j.config = JournalCtlConfiguration{} 175 err := yaml.UnmarshalStrict(yamlConfig, &j.config) 176 if err != nil { 177 return fmt.Errorf("cannot parse JournalCtlSource configuration: %w", err) 178 } 179 180 if j.config.Mode == "" { 181 j.config.Mode = configuration.TAIL_MODE 182 } 183 184 var args []string 185 if j.config.Mode == configuration.TAIL_MODE { 186 args = journalctlArgstreaming 187 } else { 188 args = journalctlArgsOneShot 189 } 190 191 if len(j.config.Filters) == 0 { 192 return fmt.Errorf("journalctl_filter is required") 193 } 194 j.args = append(args, j.config.Filters...) 195 j.src = fmt.Sprintf("journalctl-%s", strings.Join(j.config.Filters, ".")) 196 197 return nil 198 } 199 200 func (j *JournalCtlSource) Configure(yamlConfig []byte, logger *log.Entry, MetricsLevel int) error { 201 j.logger = logger 202 j.metricsLevel = MetricsLevel 203 204 err := j.UnmarshalConfig(yamlConfig) 205 if err != nil { 206 return err 207 } 208 209 return nil 210 } 211 212 func (j *JournalCtlSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error { 213 j.logger = logger 214 j.config = JournalCtlConfiguration{} 215 j.config.Mode = configuration.CAT_MODE 216 j.config.Labels = labels 217 j.config.UniqueId = uuid 218 219 //format for the DSN is : journalctl://filters=FILTER1&filters=FILTER2 220 if !strings.HasPrefix(dsn, "journalctl://") { 221 return fmt.Errorf("invalid DSN %s for journalctl source, must start with journalctl://", dsn) 222 } 223 224 qs := strings.TrimPrefix(dsn, "journalctl://") 225 if len(qs) == 0 { 226 return fmt.Errorf("empty journalctl:// DSN") 227 } 228 229 params, err := url.ParseQuery(qs) 230 if err != nil { 231 return fmt.Errorf("could not parse journalctl DSN : %s", err) 232 } 233 for key, value := range params { 234 switch key { 235 case "filters": 236 j.config.Filters = append(j.config.Filters, value...) 237 case "log_level": 238 if len(value) != 1 { 239 return fmt.Errorf("expected zero or one value for 'log_level'") 240 } 241 lvl, err := log.ParseLevel(value[0]) 242 if err != nil { 243 return fmt.Errorf("unknown level %s: %w", value[0], err) 244 } 245 j.logger.Logger.SetLevel(lvl) 246 case "since": 247 j.args = append(j.args, "--since", value[0]) 248 default: 249 return fmt.Errorf("unsupported key %s in journalctl DSN", key) 250 } 251 } 252 j.args = append(j.args, j.config.Filters...) 253 return nil 254 } 255 256 func (j *JournalCtlSource) GetMode() string { 257 return j.config.Mode 258 } 259 260 func (j *JournalCtlSource) GetName() string { 261 return "journalctl" 262 } 263 264 func (j *JournalCtlSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error { 265 defer trace.CatchPanic("crowdsec/acquis/journalctl/oneshot") 266 err := j.runJournalCtl(out, t) 267 j.logger.Debug("Oneshot journalctl acquisition is done") 268 return err 269 270 } 271 272 func (j *JournalCtlSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error { 273 t.Go(func() error { 274 defer trace.CatchPanic("crowdsec/acquis/journalctl/streaming") 275 return j.runJournalCtl(out, t) 276 }) 277 return nil 278 } 279 func (j *JournalCtlSource) CanRun() error { 280 //TODO: add a more precise check on version or something ? 281 _, err := exec.LookPath(journalctlCmd) 282 return err 283 } 284 func (j *JournalCtlSource) Dump() interface{} { 285 return j 286 }