11package io .kestra .plugin .aws .lambda ;
22
3+ import java .io .File ;
4+ import java .io .IOException ;
5+ import java .io .UncheckedIOException ;
6+ import java .net .URI ;
7+ import java .nio .file .Files ;
8+ import java .time .Duration ;
9+ import java .time .Instant ;
10+ import java .util .Map ;
11+ import java .util .Optional ;
12+
13+ import org .apache .http .HttpHeaders ;
14+ import org .apache .http .entity .ContentType ;
15+
316import com .fasterxml .jackson .core .JsonProcessingException ;
417import com .fasterxml .jackson .databind .ObjectMapper ;
518import com .google .common .annotations .VisibleForTesting ;
19+
620import io .kestra .core .exceptions .IllegalVariableEvaluationException ;
721import io .kestra .core .models .annotations .Example ;
822import io .kestra .core .models .annotations .Metric ;
1125import io .kestra .core .models .executions .metrics .Timer ;
1226import io .kestra .core .models .property .Property ;
1327import io .kestra .core .models .tasks .RunnableTask ;
28+ import io .kestra .core .models .tasks .retrys .AbstractRetry ;
1429import io .kestra .core .runners .RunContext ;
1530import io .kestra .core .serializers .JacksonMapper ;
31+ import io .kestra .core .utils .RetryUtils ;
1632import io .kestra .plugin .aws .AbstractConnection ;
1733import io .kestra .plugin .aws .ConnectionUtils ;
34+ import io .kestra .plugin .aws .cloudwatch .CloudWatchLogs ;
1835import io .kestra .plugin .aws .lambda .Invoke .Output ;
1936import io .kestra .plugin .aws .s3 .ObjectOutput ;
2037import io .swagger .v3 .oas .annotations .media .Schema ;
2542import lombok .ToString ;
2643import lombok .experimental .SuperBuilder ;
2744import lombok .extern .slf4j .Slf4j ;
28- import org .apache .http .HttpHeaders ;
29- import org .apache .http .entity .ContentType ;
3045import software .amazon .awssdk .core .SdkBytes ;
46+ import software .amazon .awssdk .services .cloudwatchlogs .CloudWatchLogsClient ;
47+ import software .amazon .awssdk .services .cloudwatchlogs .model .FilterLogEventsRequest ;
48+ import software .amazon .awssdk .services .cloudwatchlogs .model .FilteredLogEvent ;
3149import software .amazon .awssdk .services .lambda .LambdaClient ;
3250import software .amazon .awssdk .services .lambda .model .InvokeRequest ;
3351import software .amazon .awssdk .services .lambda .model .InvokeResponse ;
3452import software .amazon .awssdk .services .lambda .model .LambdaException ;
53+ import io .kestra .core .models .tasks .retrys .Exponential ;
3554
36- import java .io .File ;
37- import java .io .IOException ;
38- import java .io .UncheckedIOException ;
39- import java .net .URI ;
40- import java .nio .file .Files ;
41- import java .time .Duration ;
42- import java .util .Map ;
43- import java .util .Optional ;
55+ import java .util .List ;
4456
4557@ SuperBuilder
4658@ ToString
94106 @ Metric (name = "duration" , type = Timer .TYPE )
95107 }
96108)
109+
97110@ Slf4j
98111public class Invoke extends AbstractConnection implements RunnableTask <Output > {
99112
@@ -103,32 +116,28 @@ public class Invoke extends AbstractConnection implements RunnableTask<Output> {
103116 @ NotNull
104117 private Property <String > functionArn ;
105118
106- @ Schema (
107- title = "Function request payload." ,
108- description = "Request payload. It's a map of string -> object."
109- )
119+ @ Schema (title = "Function request payload." , description = "Request payload. It's a map of string -> object." )
110120 private Property <Map <String , Object >> functionPayload ;
111121
112122 @ Override
113123 public Output run (RunContext runContext ) throws Exception {
114124 final long start = System .nanoTime ();
125+ final Instant invocationStart = Instant .now ().minusSeconds (5 );
115126 var functionArn = runContext .render (this .functionArn ).as (String .class ).orElseThrow ();
116- var requestPayload = runContext .render (this .functionPayload ).asMap (String .class , Object .class ).isEmpty () ?
117- null :
118- runContext .render (this .functionPayload ).asMap (String .class , Object .class );
127+ var requestPayload = runContext .render (this .functionPayload ).asMap (String .class , Object .class ).isEmpty () ? null
128+ : runContext .render (this .functionPayload ).asMap (String .class , Object .class );
119129 var logger = runContext .logger ();
120130
121131 try (var lambda = client (runContext )) {
122132 var builder = InvokeRequest .builder ().functionName (functionArn );
123133 if (requestPayload != null && requestPayload .size () > 0 ) {
124- var payload = SdkBytes .fromUtf8String (OBJECT_MAPPER .writeValueAsString (requestPayload )) ;
134+ var payload = SdkBytes .fromUtf8String (OBJECT_MAPPER .writeValueAsString (requestPayload ));
125135 builder .payload (payload );
126136 }
127137 InvokeRequest request = builder .build ();
128138 // TODO take care about long-running functions: your client might disconnect during
129139 // synchronous invocation while it waits for a response. Configure your HTTP client,
130140 // SDK, firewall, proxy, or operating system to allow for long connections with timeout
131- // or keep-alive settings.
132141 InvokeResponse res = lambda .invoke (request );
133142 Optional <String > contentTypeHeader = res .sdkHttpResponse ().firstMatchingHeader (HttpHeaders .CONTENT_TYPE );
134143 ContentType contentType = parseContentType (contentTypeHeader );
@@ -141,13 +150,19 @@ public Output run(RunContext runContext) throws Exception {
141150 logger .debug ("Lambda {} invoked successfully" , functionArn );
142151 }
143152 Output out = handleContent (runContext , functionArn , contentType , res .payload ());
153+ fetchAndLogLambdaLogs (runContext , functionArn , invocationStart );
144154 runContext .metric (Timer .of ("duration" , Duration .ofNanos (System .nanoTime () - start )));
145155 return out ;
146156 } catch (LambdaException e ) {
147157 throw new LambdaInvokeException ("Lambda Invoke task execution failed for function: " + functionArn , e );
148158 }
149159 }
150160
161+ @ VisibleForTesting
162+ CloudWatchLogsClient getCloudWatchLogsClient (RunContext runContext ) throws IllegalVariableEvaluationException {
163+ return new CloudWatchLogs ().logsClient (runContext );
164+ }
165+
151166 @ VisibleForTesting
152167 LambdaClient client (final RunContext runContext ) throws IllegalVariableEvaluationException {
153168 final AwsClientConfig clientConfig = awsClientConfig (runContext );
@@ -165,7 +180,7 @@ ContentType parseContentType(Optional<String> contentType) {
165180 // Apply charset only if it was provided originally
166181 return ContentType .create (known .getMimeType (), parsed .getCharset ());
167182 }
168- } catch (Exception cte ) {
183+ } catch (Exception cte ) {
169184 log .warn ("Unable to parse Lambda response content type {}: {}" , contentType .get (), cte .getMessage ());
170185 }
171186 }
@@ -195,6 +210,53 @@ Optional<String> readError(String payload) {
195210 return Optional .empty ();
196211 }
197212
213+ @ VisibleForTesting
214+ void fetchAndLogLambdaLogs (RunContext runContext , String functionArn , Instant startTime ) {
215+ var logger = runContext .logger ();
216+ String functionName = extractFunctionName (functionArn );
217+ String logGroupName = "/aws/lambda/" + functionName ;
218+
219+ // Explicit retry policy: 5 attempts, 3s interval, maxInterval 3s
220+ AbstractRetry retryPolicy = Exponential .builder ()
221+ .interval (Duration .ofSeconds (3 ))
222+ .maxAttempts (5 )
223+ .maxInterval (Duration .ofSeconds (10 ))
224+ .build ();
225+
226+ try (CloudWatchLogsClient logsClient = getCloudWatchLogsClient (runContext )) {
227+ // Explicitly specify generic type for RetryUtils
228+ List <FilteredLogEvent > events = RetryUtils .<List <FilteredLogEvent >, Exception >of (retryPolicy , logger )
229+ .run (
230+ result -> result == null || result .isEmpty (),
231+ () -> {
232+ FilterLogEventsRequest request = FilterLogEventsRequest .builder ()
233+ .logGroupName (logGroupName )
234+ .startTime (startTime .toEpochMilli ())
235+ .build ();
236+
237+ var response = logsClient .filterLogEvents (request );
238+ return response .events ();
239+ });
240+
241+ if (events != null ) {
242+ events .forEach (event -> logger .info ("[lambda] {}" , event .message ().trim ()));
243+ }
244+
245+ } catch (Exception e ) {
246+ logger .warn ("Failed to fetch CloudWatch logs for Lambda {}: {}" , functionArn , e .getMessage ());
247+ }
248+ }
249+
250+ @ VisibleForTesting
251+ private String extractFunctionName (String functionArnOrName ) {
252+ if (functionArnOrName .contains (":function:" )) {
253+ // Handle Full ARN
254+ return functionArnOrName .split (":function:" )[1 ].split (":" )[0 ];
255+ }
256+ // Handle just the name
257+ return functionArnOrName ;
258+ }
259+
198260 @ VisibleForTesting
199261 void handleError (String functionArn , ContentType contentType , SdkBytes payload ) {
200262 String errorPayload ;
@@ -251,20 +313,14 @@ Output handleContent(RunContext runContext, String functionArn, ContentType cont
251313 @ Getter
252314 public static class Output extends ObjectOutput implements io .kestra .core .models .tasks .Output {
253315
254- @ Schema (
255- title = "Response file URI."
256- )
316+ @ Schema (title = "Response file URI." )
257317 private final URI uri ;
258318
259- @ Schema (
260- title = "Size of the response content in bytes."
261- )
319+ @ Schema (title = "Size of the response content in bytes." )
262320
263321 private final Long contentLength ;
264322
265- @ Schema (
266- title = "A standard MIME type describing the format of the content."
267- )
323+ @ Schema (title = "A standard MIME type describing the format of the content." )
268324 private final String contentType ;
269325 }
270326}
0 commit comments