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 //------------------------------------------------------------------------------