1010import static org .opensearch .sql .opensearch .executor .OpenSearchQueryManager .SQL_WORKER_THREAD_POOL_NAME ;
1111import static org .opensearch .sql .protocol .response .format .JsonResponseFormatter .Style .PRETTY ;
1212
13+ import com .google .gson .JsonElement ;
14+ import com .google .gson .JsonObject ;
15+ import com .google .gson .JsonParser ;
1316import java .util .Map ;
1417import java .util .Optional ;
1518import org .apache .calcite .rel .RelNode ;
2225import org .opensearch .analytics .exec .profile .QueryProfile ;
2326import org .opensearch .cluster .service .ClusterService ;
2427import org .opensearch .common .unit .TimeValue ;
28+ import org .opensearch .common .xcontent .XContentFactory ;
2529import org .opensearch .core .action .ActionListener ;
30+ import org .opensearch .core .xcontent .ToXContent ;
31+ import org .opensearch .core .xcontent .XContentBuilder ;
2632import org .opensearch .index .IndexSettings ;
2733import org .opensearch .indices .IndicesService ;
2834import org .opensearch .sql .api .UnifiedQueryContext ;
3844import org .opensearch .sql .executor .QueryType ;
3945import org .opensearch .sql .executor .analytics .AnalyticsExecutionEngine ;
4046import org .opensearch .sql .lang .LangSpec ;
47+ import org .opensearch .sql .monitor .profile .ProfileContext ;
48+ import org .opensearch .sql .monitor .profile .QueryProfiling ;
4149import org .opensearch .sql .plugin .transport .TransportPPLQueryResponse ;
4250import org .opensearch .sql .protocol .response .QueryResult ;
4351import org .opensearch .sql .protocol .response .format .ResponseFormatter ;
@@ -185,10 +193,9 @@ private void doExecute(
185193 // Carry the front-end task so cancellation propagates into the engine.
186194 QueryRequestContext queryCtx =
187195 withParentTask (contextProvider .getContext (), parentTask );
188- // Disable SQL-layer phase profiling when analytics engine profiling is active.
189- // Our QueryProfile (stages, tasks, timing) is strictly more detailed and replaces
190- // it.
191- UnifiedQueryContext context = buildContext (queryType , false , queryCtx );
196+
197+ UnifiedQueryContext context = buildContext (queryType , profiling , queryCtx );
198+ ProfileContext profileCtx = QueryProfiling .current ();
192199 ActionListener <TransportPPLQueryResponse > closingListener =
193200 wrapWithContextClose (context , listener );
194201 try {
@@ -205,13 +212,13 @@ private void doExecute(
205212 plan ,
206213 planContext ,
207214 queryCtx ,
208- createQueryListener (queryType , closingListener ));
215+ createQueryListener (queryType , profileCtx , closingListener ));
209216 } else {
210217 analyticsEngine .execute (
211218 plan ,
212219 planContext ,
213220 queryCtx ,
214- createQueryListener (queryType , closingListener ));
221+ createQueryListener (queryType , profileCtx , closingListener ));
215222 }
216223 } catch (Exception e ) {
217224 closingListener .onFailure (e );
@@ -361,19 +368,32 @@ private static RelNode addFetchSizeLimit(
361368 }
362369
363370 private ResponseListener <QueryResponse > createQueryListener (
364- QueryType queryType , ActionListener <TransportPPLQueryResponse > transportListener ) {
371+ QueryType queryType ,
372+ ProfileContext profileCtx ,
373+ ActionListener <TransportPPLQueryResponse > transportListener ) {
365374 ResponseFormatter <QueryResult > formatter = new SimpleJsonResponseFormatter (PRETTY );
366375 return new ResponseListener <QueryResponse >() {
367376 @ Override
368377 public void onResponse (QueryResponse response ) {
369378 LangSpec langSpec = queryType == QueryType .PPL ? PPL_SPEC : LangSpec .SQL_SPEC ;
370- String result =
371- formatter .format (
372- new QueryResult (
373- response .getSchema (), response .getResults (), response .getCursor (), langSpec ));
379+
380+ // Set the engine profile as the plan so the formatter serializes it in one pass.
374381 if (response .getProfile () != null ) {
375- // Append profile and error (if any) to the JSON response
376- result = appendProfileToJson (result , response .getProfile (), response .getError ());
382+ profileCtx .setEnginePlan (toJsonElement (response .getProfile ()));
383+ }
384+
385+ String result =
386+ QueryProfiling .withCurrentContext (
387+ profileCtx ,
388+ () ->
389+ formatter .format (
390+ new QueryResult (
391+ response .getSchema (),
392+ response .getResults (),
393+ response .getCursor (),
394+ langSpec )));
395+ if (response .getError () != null ) {
396+ result = appendError (result , response .getError ());
377397 }
378398 transportListener .onResponse (new TransportPPLQueryResponse (result ));
379399 }
@@ -385,30 +405,24 @@ public void onFailure(Exception e) {
385405 };
386406 }
387407
388- private static String appendProfileToJson ( String json , QueryProfile profile , Throwable error ) {
408+ private static JsonElement toJsonElement ( QueryProfile profile ) {
389409 try {
390- StringBuilder extra = new StringBuilder ();
391- // Append profile
392- org .opensearch .core .xcontent .XContentBuilder builder =
393- org .opensearch .common .xcontent .XContentFactory .jsonBuilder ();
394- profile .toXContent (builder , org .opensearch .core .xcontent .ToXContent .EMPTY_PARAMS );
395- extra .append (",\" profile\" :" ).append (builder .toString ());
396- // Append error if query partially failed
397- if (error != null ) {
398- extra
399- .append (",\" error\" :{\" type\" :\" " )
400- .append (error .getClass ().getSimpleName ())
401- .append ("\" ,\" reason\" :\" " )
402- .append (error .getMessage () != null ? error .getMessage ().replace ("\" " , "\\ \" " ) : "" )
403- .append ("\" }" );
404- }
405- if (json .endsWith ("}" )) {
406- return json .substring (0 , json .length () - 1 ) + extra + "}" ;
407- }
408- return json ;
410+ XContentBuilder builder = XContentFactory .jsonBuilder ();
411+ profile .toXContent (builder , ToXContent .EMPTY_PARAMS );
412+ return JsonParser .parseString (builder .toString ());
409413 } catch (Exception e ) {
414+ return null ;
415+ }
416+ }
417+
418+ private static String appendError (String json , Throwable error ) {
419+ if (!json .endsWith ("}" )) {
410420 return json ;
411421 }
422+ JsonObject err = new JsonObject ();
423+ err .addProperty ("type" , error .getClass ().getSimpleName ());
424+ err .addProperty ("reason" , error .getMessage () != null ? error .getMessage () : "" );
425+ return json .substring (0 , json .length () - 1 ) + ",\" error\" :" + err + "}" ;
412426 }
413427
414428 private static Runnable withCurrentContext (final Runnable task ) {
0 commit comments