github.com/Jeffail/benthos/v3@v3.65.0/lib/processor/jmespath.go (about)

     1  package processor
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/Jeffail/benthos/v3/internal/docs"
     9  	"github.com/Jeffail/benthos/v3/internal/tracing"
    10  	"github.com/Jeffail/benthos/v3/lib/log"
    11  	"github.com/Jeffail/benthos/v3/lib/metrics"
    12  	"github.com/Jeffail/benthos/v3/lib/types"
    13  	jmespath "github.com/jmespath/go-jmespath"
    14  )
    15  
    16  //------------------------------------------------------------------------------
    17  
    18  func init() {
    19  	Constructors[TypeJMESPath] = TypeSpec{
    20  		constructor: NewJMESPath,
    21  		Categories: []Category{
    22  			CategoryMapping,
    23  		},
    24  		Summary: `
    25  Executes a [JMESPath query](http://jmespath.org/) on JSON documents and replaces
    26  the message with the resulting document.`,
    27  		Description: `
    28  :::note Try out Bloblang
    29  For better performance and improved capabilities try out native Benthos mapping with the [bloblang processor](/docs/components/processors/bloblang).
    30  :::
    31  `,
    32  		Examples: []docs.AnnotatedExample{
    33  			{
    34  				Title: "Mapping",
    35  				Summary: `
    36  When receiving JSON documents of the form:
    37  
    38  ` + "```json" + `
    39  {
    40    "locations": [
    41      {"name": "Seattle", "state": "WA"},
    42      {"name": "New York", "state": "NY"},
    43      {"name": "Bellevue", "state": "WA"},
    44      {"name": "Olympia", "state": "WA"}
    45    ]
    46  }
    47  ` + "```" + `
    48  
    49  We could collapse the location names from the state of Washington into a field ` + "`Cities`" + `:
    50  
    51  ` + "```json" + `
    52  {"Cities": "Bellevue, Olympia, Seattle"}
    53  ` + "```" + `
    54  
    55  With the following config:`,
    56  				Config: `
    57  pipeline:
    58    processors:
    59      - jmespath:
    60          query: "locations[?state == 'WA'].name | sort(@) | {Cities: join(', ', @)}"
    61  `,
    62  			},
    63  		},
    64  		FieldSpecs: docs.FieldSpecs{
    65  			docs.FieldCommon("query", "The JMESPath query to apply to messages."),
    66  			PartsFieldSpec,
    67  		},
    68  	}
    69  }
    70  
    71  //------------------------------------------------------------------------------
    72  
    73  // JMESPathConfig contains configuration fields for the JMESPath processor.
    74  type JMESPathConfig struct {
    75  	Parts []int  `json:"parts" yaml:"parts"`
    76  	Query string `json:"query" yaml:"query"`
    77  }
    78  
    79  // NewJMESPathConfig returns a JMESPathConfig with default values.
    80  func NewJMESPathConfig() JMESPathConfig {
    81  	return JMESPathConfig{
    82  		Parts: []int{},
    83  		Query: "",
    84  	}
    85  }
    86  
    87  //------------------------------------------------------------------------------
    88  
    89  // JMESPath is a processor that executes JMESPath queries on a message part and
    90  // replaces the contents with the result.
    91  type JMESPath struct {
    92  	parts []int
    93  	query *jmespath.JMESPath
    94  
    95  	conf  Config
    96  	log   log.Modular
    97  	stats metrics.Type
    98  
    99  	mCount     metrics.StatCounter
   100  	mErrJSONP  metrics.StatCounter
   101  	mErrJMES   metrics.StatCounter
   102  	mErrJSONS  metrics.StatCounter
   103  	mErr       metrics.StatCounter
   104  	mSent      metrics.StatCounter
   105  	mBatchSent metrics.StatCounter
   106  }
   107  
   108  // NewJMESPath returns a JMESPath processor.
   109  func NewJMESPath(
   110  	conf Config, mgr types.Manager, log log.Modular, stats metrics.Type,
   111  ) (Type, error) {
   112  	query, err := jmespath.Compile(conf.JMESPath.Query)
   113  	if err != nil {
   114  		return nil, fmt.Errorf("failed to compile JMESPath query: %v", err)
   115  	}
   116  	j := &JMESPath{
   117  		parts: conf.JMESPath.Parts,
   118  		query: query,
   119  		conf:  conf,
   120  		log:   log,
   121  		stats: stats,
   122  
   123  		mCount:     stats.GetCounter("count"),
   124  		mErrJSONP:  stats.GetCounter("error.json_parse"),
   125  		mErrJMES:   stats.GetCounter("error.jmespath_search"),
   126  		mErrJSONS:  stats.GetCounter("error.json_set"),
   127  		mErr:       stats.GetCounter("error"),
   128  		mSent:      stats.GetCounter("sent"),
   129  		mBatchSent: stats.GetCounter("batch.sent"),
   130  	}
   131  	return j, nil
   132  }
   133  
   134  //------------------------------------------------------------------------------
   135  
   136  func safeSearch(part interface{}, j *jmespath.JMESPath) (res interface{}, err error) {
   137  	defer func() {
   138  		if r := recover(); r != nil {
   139  			err = fmt.Errorf("jmespath panic: %v", r)
   140  		}
   141  	}()
   142  	return j.Search(part)
   143  }
   144  
   145  // JMESPath doesn't like json.Number so we walk the tree and replace them.
   146  func clearNumbers(v interface{}) (interface{}, bool) {
   147  	switch t := v.(type) {
   148  	case map[string]interface{}:
   149  		for k, v := range t {
   150  			if nv, ok := clearNumbers(v); ok {
   151  				t[k] = nv
   152  			}
   153  		}
   154  	case []interface{}:
   155  		for i, v := range t {
   156  			if nv, ok := clearNumbers(v); ok {
   157  				t[i] = nv
   158  			}
   159  		}
   160  	case json.Number:
   161  		f, err := t.Float64()
   162  		if err != nil {
   163  			if i, err := t.Int64(); err == nil {
   164  				return i, true
   165  			}
   166  		}
   167  		return f, true
   168  	}
   169  	return nil, false
   170  }
   171  
   172  // ProcessMessage applies the processor to a message, either creating >0
   173  // resulting messages or a response to be sent back to the message source.
   174  func (p *JMESPath) ProcessMessage(msg types.Message) ([]types.Message, types.Response) {
   175  	p.mCount.Incr(1)
   176  	newMsg := msg.Copy()
   177  
   178  	proc := func(index int, span *tracing.Span, part types.Part) error {
   179  		jsonPart, err := part.JSON()
   180  		if err != nil {
   181  			p.mErrJSONP.Incr(1)
   182  			p.mErr.Incr(1)
   183  			p.log.Debugf("Failed to parse part into json: %v\n", err)
   184  			return err
   185  		}
   186  		if v, replace := clearNumbers(jsonPart); replace {
   187  			jsonPart = v
   188  		}
   189  
   190  		var result interface{}
   191  		if result, err = safeSearch(jsonPart, p.query); err != nil {
   192  			p.mErrJMES.Incr(1)
   193  			p.mErr.Incr(1)
   194  			p.log.Debugf("Failed to search json: %v\n", err)
   195  			return err
   196  		}
   197  
   198  		if err = newMsg.Get(index).SetJSON(result); err != nil {
   199  			p.mErrJSONS.Incr(1)
   200  			p.mErr.Incr(1)
   201  			p.log.Debugf("Failed to convert jmespath result into part: %v\n", err)
   202  			return err
   203  		}
   204  		return nil
   205  	}
   206  
   207  	IteratePartsWithSpanV2(TypeJMESPath, p.parts, newMsg, proc)
   208  
   209  	msgs := [1]types.Message{newMsg}
   210  
   211  	p.mBatchSent.Incr(1)
   212  	p.mSent.Incr(int64(newMsg.Len()))
   213  	return msgs[:], nil
   214  }
   215  
   216  // CloseAsync shuts down the processor and stops processing requests.
   217  func (p *JMESPath) CloseAsync() {
   218  }
   219  
   220  // WaitForClose blocks until the processor has closed down.
   221  func (p *JMESPath) WaitForClose(timeout time.Duration) error {
   222  	return nil
   223  }
   224  
   225  //------------------------------------------------------------------------------