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