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  }