7
7
8
8
import org .apache .logging .log4j .message .ParameterizedMessage ;
9
9
import org .apache .logging .log4j .util .Supplier ;
10
+ import org .elasticsearch .ElasticsearchException ;
10
11
import org .elasticsearch .ElasticsearchParseException ;
11
12
import org .elasticsearch .common .Nullable ;
13
+ import org .elasticsearch .common .Strings ;
12
14
import org .elasticsearch .common .unit .TimeValue ;
15
+ import org .elasticsearch .common .xcontent .ObjectPath ;
13
16
import org .elasticsearch .common .xcontent .ToXContentObject ;
14
17
import org .elasticsearch .common .xcontent .XContentBuilder ;
15
18
import org .elasticsearch .common .xcontent .XContentParser ;
16
19
import org .elasticsearch .license .XPackLicenseState ;
20
+ import org .elasticsearch .script .JodaCompatibleZonedDateTime ;
17
21
import org .elasticsearch .xpack .core .watcher .actions .throttler .ActionThrottler ;
18
22
import org .elasticsearch .xpack .core .watcher .actions .throttler .Throttler ;
19
23
import org .elasticsearch .xpack .core .watcher .actions .throttler .ThrottlerField ;
30
34
import java .time .Clock ;
31
35
import java .time .ZoneOffset ;
32
36
import java .time .ZonedDateTime ;
37
+ import java .util .ArrayList ;
38
+ import java .util .Collection ;
39
+ import java .util .HashMap ;
40
+ import java .util .List ;
41
+ import java .util .Map ;
33
42
import java .util .Objects ;
43
+ import java .util .Set ;
44
+ import java .util .stream .Collectors ;
34
45
35
46
import static org .elasticsearch .common .unit .TimeValue .timeValueMillis ;
36
47
37
48
public class ActionWrapper implements ToXContentObject {
38
49
50
+ private final int MAXIMUM_FOREACH_RUNS = 100 ;
51
+
39
52
private String id ;
40
53
@ Nullable
41
54
private final ExecutableCondition condition ;
42
55
@ Nullable
43
56
private final ExecutableTransform <Transform , Transform .Result > transform ;
44
57
private final ActionThrottler throttler ;
45
58
private final ExecutableAction <? extends Action > action ;
59
+ @ Nullable
60
+ private String path ;
46
61
47
62
public ActionWrapper (String id , ActionThrottler throttler ,
48
63
@ Nullable ExecutableCondition condition ,
49
64
@ Nullable ExecutableTransform <Transform , Transform .Result > transform ,
50
- ExecutableAction <? extends Action > action ) {
65
+ ExecutableAction <? extends Action > action ,
66
+ @ Nullable String path ) {
51
67
this .id = id ;
52
68
this .condition = condition ;
53
69
this .throttler = throttler ;
54
70
this .transform = transform ;
55
71
this .action = action ;
72
+ this .path = path ;
56
73
}
57
74
58
75
public String id () {
@@ -140,16 +157,90 @@ public ActionWrapperResult execute(WatchExecutionContext ctx) {
140
157
return new ActionWrapperResult (id , conditionResult , null , new Action .Result .FailureWithException (action .type (), e ));
141
158
}
142
159
}
143
- try {
144
- Action .Result actionResult = action .execute (id , ctx , payload );
145
- return new ActionWrapperResult (id , conditionResult , transformResult , actionResult );
146
- } catch (Exception e ) {
147
- action .logger ().error (
160
+ if (Strings .isEmpty (path )) {
161
+ try {
162
+ Action .Result actionResult = action .execute (id , ctx , payload );
163
+ return new ActionWrapperResult (id , conditionResult , transformResult , actionResult );
164
+ } catch (Exception e ) {
165
+ action .logger ().error (
166
+ (Supplier <?>) () -> new ParameterizedMessage ("failed to execute action [{}/{}]" , ctx .watch ().id (), id ), e );
167
+ return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
168
+ }
169
+ } else {
170
+ try {
171
+ List <Action .Result > results = new ArrayList <>();
172
+ Object object = ObjectPath .eval (path , toMap (ctx ));
173
+ int runs = 0 ;
174
+ if (object instanceof Collection ) {
175
+ Collection collection = Collection .class .cast (object );
176
+ if (collection .isEmpty ()) {
177
+ throw new ElasticsearchException ("foreach object [{}] was an empty list, could not run any action" , path );
178
+ } else {
179
+ for (Object o : collection ) {
180
+ if (runs >= MAXIMUM_FOREACH_RUNS ) {
181
+ break ;
182
+ }
183
+ if (o instanceof Map ) {
184
+ results .add (action .execute (id , ctx , new Payload .Simple ((Map <String , Object >) o )));
185
+ } else {
186
+ results .add (action .execute (id , ctx , new Payload .Simple ("_value" , o )));
187
+ }
188
+ runs ++;
189
+ }
190
+ }
191
+ } else if (object == null ) {
192
+ throw new ElasticsearchException ("specified foreach object was null: [{}]" , path );
193
+ } else {
194
+ throw new ElasticsearchException ("specified foreach object was not a an array/collection: [{}]" , path );
195
+ }
196
+
197
+ // check if we have mixed results, then set to partial failure
198
+ final Set <Action .Result .Status > statuses = results .stream ().map (Action .Result ::status ).collect (Collectors .toSet ());
199
+ Action .Result .Status status ;
200
+ if (statuses .size () == 1 ) {
201
+ status = statuses .iterator ().next ();
202
+ } else {
203
+ status = Action .Result .Status .PARTIAL_FAILURE ;
204
+ }
205
+
206
+ final int numberOfActionsExecuted = runs ;
207
+ return new ActionWrapperResult (id , conditionResult , transformResult ,
208
+ new Action .Result (action .type (), status ) {
209
+ @ Override
210
+ public XContentBuilder toXContent (XContentBuilder builder , Params params ) throws IOException {
211
+ builder .field ("number_of_actions_executed" , numberOfActionsExecuted );
212
+ builder .startArray (WatchField .FOREACH .getPreferredName ());
213
+ for (Action .Result result : results ) {
214
+ builder .startObject ();
215
+ result .toXContent (builder , params );
216
+ builder .endObject ();
217
+ }
218
+ builder .endArray ();
219
+ return builder ;
220
+ }
221
+ });
222
+ } catch (Exception e ) {
223
+ action .logger ().error (
148
224
(Supplier <?>) () -> new ParameterizedMessage ("failed to execute action [{}/{}]" , ctx .watch ().id (), id ), e );
149
- return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
225
+ return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
226
+ }
150
227
}
151
228
}
152
229
230
+ private Map <String , Object > toMap (WatchExecutionContext ctx ) {
231
+ Map <String , Object > model = new HashMap <>();
232
+ model .put ("id" , ctx .id ().value ());
233
+ model .put ("watch_id" , ctx .id ().watchId ());
234
+ model .put ("execution_time" , new JodaCompatibleZonedDateTime (ctx .executionTime ().toInstant (), ZoneOffset .UTC ));
235
+ model .put ("trigger" , ctx .triggerEvent ().data ());
236
+ model .put ("metadata" , ctx .watch ().metadata ());
237
+ model .put ("vars" , ctx .vars ());
238
+ if (ctx .payload ().data () != null ) {
239
+ model .put ("payload" , ctx .payload ().data ());
240
+ }
241
+ return Map .of ("ctx" , model );
242
+ }
243
+
153
244
@ Override
154
245
public boolean equals (Object o ) {
155
246
if (this == o ) return true ;
@@ -186,6 +277,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
186
277
.field (transform .type (), transform , params )
187
278
.endObject ();
188
279
}
280
+ if (Strings .isEmpty (path ) == false ) {
281
+ builder .field (WatchField .FOREACH .getPreferredName (), path );
282
+ }
189
283
builder .field (action .type (), action , params );
190
284
return builder .endObject ();
191
285
}
@@ -198,6 +292,7 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
198
292
ExecutableCondition condition = null ;
199
293
ExecutableTransform <Transform , Transform .Result > transform = null ;
200
294
TimeValue throttlePeriod = null ;
295
+ String path = null ;
201
296
ExecutableAction <? extends Action > action = null ;
202
297
203
298
String currentFieldName = null ;
@@ -208,6 +303,8 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
208
303
} else {
209
304
if (WatchField .CONDITION .match (currentFieldName , parser .getDeprecationHandler ())) {
210
305
condition = actionRegistry .getConditionRegistry ().parseExecutable (watchId , parser );
306
+ } else if (WatchField .FOREACH .match (currentFieldName , parser .getDeprecationHandler ())) {
307
+ path = parser .text ();
211
308
} else if (Transform .TRANSFORM .match (currentFieldName , parser .getDeprecationHandler ())) {
212
309
transform = actionRegistry .getTransformRegistry ().parse (watchId , parser );
213
310
} else if (ThrottlerField .THROTTLE_PERIOD .match (currentFieldName , parser .getDeprecationHandler ())) {
@@ -235,7 +332,7 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
235
332
}
236
333
237
334
ActionThrottler throttler = new ActionThrottler (clock , throttlePeriod , licenseState );
238
- return new ActionWrapper (actionId , throttler , condition , transform , action );
335
+ return new ActionWrapper (actionId , throttler , condition , transform , action , path );
239
336
}
240
337
241
338
}
0 commit comments