github.com/magnusbaeck/logstash-filter-verifier/v2@v2.0.0-pre.1/logstash/parallel_process.go (about) 1 // Copyright (c) 2015-2016 Magnus Bäck <magnus@noun.se> 2 3 package logstash 4 5 import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "regexp" 16 "strconv" 17 "strings" 18 "time" 19 ) 20 21 // TestStream contains the input and output streams for one test case. 22 type TestStream struct { 23 sender *net.UnixConn 24 senderListener *net.UnixListener 25 senderReady chan struct{} 26 senderPath string 27 receiver *deletedTempFile 28 timeout time.Duration 29 30 inputCodec string 31 fields FieldSet 32 } 33 34 // NewTestStream creates a TestStream, inputCodec is 35 // the desired codec for the stdin input and inputType the value of 36 // the "type" field for ingested events. 37 // The timeout defines, how long to wait in Write for the receiver to 38 // become available. 39 func NewTestStream(inputCodec string, fields FieldSet, timeout time.Duration) (*TestStream, error) { 40 dir, err := ioutil.TempDir("", "") 41 if err != nil { 42 return nil, err 43 } 44 45 ts := &TestStream{ 46 senderReady: make(chan struct{}), 47 senderPath: filepath.Join(dir, "socket"), 48 inputCodec: inputCodec, 49 fields: fields, 50 timeout: timeout, 51 } 52 53 ts.senderListener, err = net.ListenUnix("unix", &net.UnixAddr{Name: ts.senderPath, Net: "unix"}) 54 if err != nil { 55 log.Fatalf("Unable to create unix socket for listening: %s", err) 56 } 57 ts.senderListener.SetUnlinkOnClose(false) 58 59 go func() { 60 defer close(ts.senderReady) 61 62 ts.sender, err = ts.senderListener.AcceptUnix() 63 if err != nil { 64 log.Errorf("Error while accept unix socket: %s", err) 65 } 66 ts.senderListener.Close() 67 }() 68 69 // Unfortunately Logstash doesn't make it easy to just read 70 // events from a stdout-connected pipe and the log from a 71 // stderr-connected pipe. Stdout can contain other garbage (at 72 // the very least "future logs will be sent to ...") and error 73 // messages could very well be sent there too. Mitigate by 74 // having Logstash write output logs to a temporary file and 75 // its own logs to a different temporary file. 76 outputFile, err := newDeletedTempFile("", "") 77 if err != nil { 78 return nil, err 79 } 80 ts.receiver = outputFile 81 82 return ts, nil 83 } 84 85 // Write writes to the sender of the TestStream. 86 func (ts *TestStream) Write(p []byte) (n int, err error) { 87 timer := time.NewTimer(ts.timeout) 88 select { 89 case <-ts.senderReady: 90 case <-timer.C: 91 return 0, fmt.Errorf("Write timeout error") 92 } 93 return ts.sender.Write(p) 94 } 95 96 // Close closes the sender of the TestStream. 97 func (ts *TestStream) Close() error { 98 if ts.sender != nil { 99 err := ts.sender.Close() 100 ts.sender = nil 101 return err 102 } 103 return nil 104 } 105 106 // Cleanup closes and removes all temporary resources 107 // for a TestStream. 108 func (ts *TestStream) Cleanup() { 109 if ts.senderListener != nil { 110 ts.senderListener.Close() 111 } 112 if ts.sender != nil { 113 ts.Close() 114 } 115 os.RemoveAll(filepath.Dir(ts.senderPath)) 116 if ts.receiver != nil { 117 ts.receiver.Close() 118 } 119 } 120 121 // CleanupTestStreams closes all sockets and streams as well 122 // removes temporary file ressources for an array of 123 // TestStreams. 124 func CleanupTestStreams(ts []*TestStream) { 125 for i := range ts { 126 ts[i].Cleanup() 127 } 128 } 129 130 // ParallelProcess represents the invocation and execution of a Logstash child 131 // process that emits JSON events from multiple inputs through filter to multiple outputs 132 // configuration files supplied by the caller. 133 type ParallelProcess struct { 134 streams []*TestStream 135 136 child *exec.Cmd 137 inv *Invocation 138 139 stdio io.Reader 140 } 141 142 // getSocketInOutPlugins returns arrays of strings with the Logstash 143 // input and output plugins, respectively, that should be included in 144 // the Logstash configuration used for the supplied array of 145 // TestStream structs. 146 // 147 // Each item in the returned array corresponds to the TestStream with 148 // the same index. 149 func getSocketInOutPlugins(testStream []*TestStream) ([]string, []string, error) { 150 logstashInput := make([]string, len(testStream)) 151 logstashOutput := make([]string, len(testStream)) 152 153 for i, sp := range testStream { 154 // Populate the [@metadata][__lfv_testcase] field with 155 // the testcase index so that we can route messages 156 // from each testcase to the right output stream. 157 if metadataField, exists := sp.fields["@metadata"]; exists { 158 if metadataSubfields, ok := metadataField.(map[string]interface{}); ok { 159 metadataSubfields["__lfv_testcase"] = strconv.Itoa(i) 160 } else { 161 return nil, nil, fmt.Errorf("the supplied contents of the @metadata field must be a hash (found %T instead)", metadataField) 162 } 163 } else { 164 sp.fields["@metadata"] = map[string]interface{}{"__lfv_testcase": strconv.Itoa(i)} 165 } 166 167 fieldHash, err := sp.fields.LogstashHash() 168 if err != nil { 169 return nil, nil, err 170 } 171 logstashInput[i] = fmt.Sprintf("unix { mode => \"client\" path => %q codec => %s add_field => %s }", 172 sp.senderPath, sp.inputCodec, fieldHash) 173 logstashOutput[i] = fmt.Sprintf("if [@metadata][__lfv_testcase] == \"%s\" { file { path => %q codec => \"json_lines\" } }", 174 strconv.Itoa(i), sp.receiver.Name()) 175 } 176 return logstashInput, logstashOutput, nil 177 } 178 179 // NewParallelProcess prepares for the execution of a new Logstash process but 180 // doesn't actually start it. logstashPath is the path to the Logstash 181 // executable (typically /opt/logstash/bin/logstash). The configs parameter is 182 // one or more configuration files containing Logstash filters. 183 func NewParallelProcess(inv *Invocation, testStream []*TestStream, keptEnvVars []string) (*ParallelProcess, error) { 184 logstashInput, logstashOutput, err := getSocketInOutPlugins(testStream) 185 if err != nil { 186 CleanupTestStreams(testStream) 187 return nil, err 188 } 189 190 env := getLimitedEnvironment(os.Environ(), keptEnvVars) 191 inputs := fmt.Sprintf("input { %s } ", strings.Join(logstashInput, " ")) 192 outputs := fmt.Sprintf("output { %s }", strings.Join(logstashOutput, " ")) 193 args, err := inv.Args(inputs, outputs) 194 if err != nil { 195 CleanupTestStreams(testStream) 196 return nil, err 197 } 198 p, err := newParallelProcessWithArgs(inv.LogstashPath, args, env) 199 if err != nil { 200 CleanupTestStreams(testStream) 201 return nil, err 202 } 203 p.inv = inv 204 p.streams = testStream 205 return p, nil 206 } 207 208 // newParallelProcessWithArgs performs the non-Logstash specific low-level 209 // actions of preparing to spawn a child process, making it easier to 210 // test the code in this package. 211 func newParallelProcessWithArgs(command string, args []string, env []string) (*ParallelProcess, error) { 212 c := exec.Command(command, args...) 213 c.Env = env 214 215 // Save the process's stdout and stderr since an early startup 216 // failure (e.g. JVM issues) will get dumped there and not in 217 // the log file. 218 var b bytes.Buffer 219 c.Stdout = &b 220 c.Stderr = &b 221 222 return &ParallelProcess{ 223 child: c, 224 stdio: &b, 225 }, nil 226 } 227 228 // Start starts a Logstash child process with the previously supplied 229 // configuration. 230 func (p *ParallelProcess) Start() error { 231 log.Infof("Starting %q with args %q.", p.child.Path, p.child.Args[1:]) 232 return p.child.Start() 233 } 234 235 // Wait blocks until the started Logstash process terminates and 236 // returns the result of the execution. 237 func (p *ParallelProcess) Wait() (*ParallelResult, error) { 238 if p.child.Process == nil { 239 return nil, errors.New("can't wait on an unborn process") 240 } 241 log.Debugf("Waiting for child with pid %d to terminate.", p.child.Process.Pid) 242 243 waiterr := p.child.Wait() 244 245 // Save the log output regardless of whether the child process 246 // succeeded or not. 247 logbuf, logerr := ioutil.ReadAll(p.inv.logFile) 248 if logerr != nil { 249 // Log this weird error condition but don't let it 250 // fail the function. We don't care about the log 251 // contents unless Logstash fails, in which we'll 252 // report that problem anyway. 253 log.Errorf("Error reading the Logstash logfile: %s", logerr) 254 } 255 outbuf, _ := ioutil.ReadAll(p.stdio) 256 257 result := ParallelResult{ 258 Events: [][]Event{}, 259 Log: string(logbuf), 260 Output: string(outbuf), 261 Success: waiterr == nil, 262 } 263 if waiterr != nil { 264 re := regexp.MustCompile("An unexpected error occurred.*closed stream.*IOError") 265 if re.MatchString(result.Log) { 266 log.Warning("Workaround for IOError in unix.rb on stop, process result anyway. (see https://github.com/logstash-plugins/logstash-input-unix/pull/18)") 267 result.Success = true 268 } else { 269 return &result, waiterr 270 } 271 } 272 273 var err error 274 result.Events = make([][]Event, len(p.streams)) 275 for i, tc := range p.streams { 276 result.Events[i], err = readEvents(tc.receiver) 277 tc.receiver.Close() 278 result.Success = err == nil 279 280 // Logstash's unix input adds a "path" field 281 // containing the socket path, which screws up the 282 // test results. We can't unconditionally delete that 283 // field because the input JSON payload could contain 284 // a "path" field that we can't touch, but we can 285 // safely delete the field if its contents if equal to 286 // the socket path. 287 for j := range result.Events[i] { 288 if path, exists := result.Events[i][j]["path"]; exists && path == p.streams[i].senderPath { 289 delete(result.Events[i][j], "path") 290 } 291 } 292 } 293 return &result, err 294 } 295 296 // Release frees all allocated resources connected to this process. 297 func (p *ParallelProcess) Release() { 298 CleanupTestStreams(p.streams) 299 }