API Logs!

This commit is contained in:
2023-03-31 16:29:30 -05:00
parent 084630918f
commit 6fbfbe9db2
4 changed files with 649 additions and 26 deletions

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.javalin;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@ -37,6 +38,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
import com.kingsrook.qqq.api.actions.QRecordApiAdapter;
import com.kingsrook.qqq.api.model.APILog;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
@ -276,7 +278,21 @@ public class QJavalinApiHandler
*******************************************************************************/
private static void doPathNotFound(Context context)
{
handleException(context, new QNotFoundException("Could not find any resources at path " + context.path()));
APILog apiLog = newAPILog(context);
try
{
setupSession(context, null, null);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////
// if we don't have a session, we won't be able to store the api log... //
//////////////////////////////////////////////////////////////////////////
LOG.debug("No session in a 404; will not create api log", e);
}
handleException(context, new QNotFoundException("Could not find any resources at path " + context.path()), apiLog);
}
@ -548,6 +564,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
String primaryKey = context.pathParam("primaryKey");
APILog apiLog = newAPILog(context);
try
{
@ -585,12 +602,64 @@ public class QJavalinApiHandler
Map<String, Serializable> outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, version);
QJavalinAccessLogger.logEndSuccess();
context.result(JsonUtils.toJson(outputRecord));
String resultString = JsonUtils.toJson(outputRecord);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static APILog newAPILog(Context context)
{
APILog apiLog = new APILog()
.withTimestamp(Instant.now())
.withMethod(context.req().getMethod())
.withPath(context.path())
.withQueryString(context.queryString())
.withRequestBody(context.body());
try
{
apiLog.setVersion(context.pathParam("version"));
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////
// pathParam throws if the param isn't found - in that case, just leave it null //
//////////////////////////////////////////////////////////////////////////////////
}
return (apiLog);
}
/*******************************************************************************
**
*******************************************************************************/
private static void storeApiLog(APILog apiLog)
{
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(APILog.TABLE_NAME);
// todo - security fields!!!!!
// todo - user!!!!
insertInput.setRecords(List.of(apiLog.toQRecord()));
new InsertAction().executeAsync(insertInput);
}
catch(Exception e)
{
LOG.warn("Error storing API log", e);
}
}
@ -604,6 +673,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
QQueryFilter filter = null;
APILog apiLog = newAPILog(context);
try
{
@ -822,12 +892,14 @@ public class QJavalinApiHandler
output.put("records", records);
QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS));
context.result(JsonUtils.toJson(output));
String resultString = JsonUtils.toJson(output);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e, logPair("filter", filter));
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1041,6 +1113,7 @@ public class QJavalinApiHandler
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
APILog apiLog = newAPILog(context);
try
{
@ -1090,12 +1163,14 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.CREATED.getCode());
context.result(JsonUtils.toJson(outputRecord));
String resultString = JsonUtils.toJson(outputRecord);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1108,6 +1183,7 @@ public class QJavalinApiHandler
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
APILog apiLog = newAPILog(context);
try
{
@ -1196,12 +1272,14 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
context.result(JsonUtils.toJson(response));
String resultString = JsonUtils.toJson(response);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1214,6 +1292,7 @@ public class QJavalinApiHandler
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
APILog apiLog = newAPILog(context);
try
{
@ -1325,12 +1404,14 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
context.result(JsonUtils.toJson(response));
String resultString = JsonUtils.toJson(response);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1353,6 +1434,7 @@ public class QJavalinApiHandler
{
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
APILog apiLog = newAPILog(context);
try
{
@ -1463,12 +1545,14 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
context.result(JsonUtils.toJson(response));
String resultString = JsonUtils.toJson(response);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1482,6 +1566,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
String primaryKey = context.pathParam("primaryKey");
APILog apiLog = newAPILog(context);
try
{
@ -1546,11 +1631,12 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.NO_CONTENT.getCode());
storeApiLog(apiLog.withStatusCode(context.statusCode()));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
@ -1564,6 +1650,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
String tableApiName = context.pathParam("tableName");
String primaryKey = context.pathParam("primaryKey");
APILog apiLog = newAPILog(context);
try
{
@ -1599,22 +1686,33 @@ public class QJavalinApiHandler
QJavalinAccessLogger.logEndSuccess();
context.status(HttpStatus.Code.NO_CONTENT.getCode());
storeApiLog(apiLog.withStatusCode(context.statusCode()));
}
catch(Exception e)
{
QJavalinAccessLogger.logEndFail(e);
handleException(context, e);
handleException(context, e, apiLog);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void handleException(Context context, Exception e, APILog apiLog)
{
handleException(null, context, e, apiLog);
}
/*******************************************************************************
**
*******************************************************************************/
public static void handleException(Context context, Exception e)
{
handleException(null, context, e);
handleException(null, context, e, null);
}
@ -1622,13 +1720,13 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
public static void handleException(HttpStatus.Code statusCode, Context context, Exception e, APILog apiLog)
{
QBadRequestException badRequestException = ExceptionUtils.findClassInRootChain(e, QBadRequestException.class);
if(badRequestException != null)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
respondWithError(context, statusCode, badRequestException.getMessage());
respondWithError(context, statusCode, badRequestException.getMessage(), apiLog);
return;
}
@ -1638,26 +1736,28 @@ public class QJavalinApiHandler
if(userFacingException instanceof QNotFoundException)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404
respondWithError(context, statusCode, userFacingException.getMessage());
respondWithError(context, statusCode, userFacingException.getMessage(), apiLog);
return;
}
else
{
LOG.info("User-facing exception", e);
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500
respondWithError(context, statusCode, userFacingException.getMessage());
respondWithError(context, statusCode, userFacingException.getMessage(), apiLog);
return;
}
}
else
{
if(e instanceof QAuthenticationException)
{
respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401
respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage(), apiLog); // 401
return;
}
if(e instanceof QPermissionDeniedException)
{
respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage(), apiLog); // 403
return;
}
@ -1665,7 +1765,8 @@ public class QJavalinApiHandler
// default exception handling //
////////////////////////////////
LOG.warn("Exception in javalin request", e);
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")", apiLog); // 500
return;
}
}
@ -1674,7 +1775,7 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage)
public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage, APILog apiLog)
{
context.status(statusCode.getCode());
@ -1695,7 +1796,17 @@ public class QJavalinApiHandler
///////////////////////////
}
context.result(JsonUtils.toJson(Map.of("error", errorMessage)));
String responseBody = JsonUtils.toJson(Map.of("error", errorMessage));
context.result(responseBody);
if(apiLog != null)
{
if(QContext.getQSession() != null && QContext.getQInstance() != null)
{
apiLog.withStatusCode(statusCode.getCode()).withResponseBody(responseBody);
storeApiLog(apiLog);
}
}
}
}

View File

@ -0,0 +1,393 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
**
*******************************************************************************/
public class APILog extends QRecordEntity
{
public static final String TABLE_NAME = "apiLog";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant timestamp;
@QField()
private String method;
@QField()
private Integer statusCode;
@QField()
private String version;
@QField()
private String path;
@QField()
private String queryString;
@QField()
private String requestBody;
@QField()
private String responseBody;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public APILog()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public APILog(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return id;
}
/*******************************************************************************
** Setter for id
**
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
**
*******************************************************************************/
public APILog withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for timestamp
**
*******************************************************************************/
public Instant getTimestamp()
{
return timestamp;
}
/*******************************************************************************
** Setter for timestamp
**
*******************************************************************************/
public void setTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
}
/*******************************************************************************
** Fluent setter for timestamp
**
*******************************************************************************/
public APILog withTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
return (this);
}
/*******************************************************************************
** Getter for method
**
*******************************************************************************/
public String getMethod()
{
return method;
}
/*******************************************************************************
** Setter for method
**
*******************************************************************************/
public void setMethod(String method)
{
this.method = method;
}
/*******************************************************************************
** Fluent setter for method
**
*******************************************************************************/
public APILog withMethod(String method)
{
this.method = method;
return (this);
}
/*******************************************************************************
** Getter for statusCode
**
*******************************************************************************/
public Integer getStatusCode()
{
return statusCode;
}
/*******************************************************************************
** Setter for statusCode
**
*******************************************************************************/
public void setStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
}
/*******************************************************************************
** Fluent setter for statusCode
**
*******************************************************************************/
public APILog withStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
return (this);
}
/*******************************************************************************
** Getter for version
**
*******************************************************************************/
public String getVersion()
{
return version;
}
/*******************************************************************************
** Setter for version
**
*******************************************************************************/
public void setVersion(String version)
{
this.version = version;
}
/*******************************************************************************
** Fluent setter for version
**
*******************************************************************************/
public APILog withVersion(String version)
{
this.version = version;
return (this);
}
/*******************************************************************************
** Getter for path
**
*******************************************************************************/
public String getPath()
{
return path;
}
/*******************************************************************************
** Setter for path
**
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
**
*******************************************************************************/
public APILog withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for queryString
**
*******************************************************************************/
public String getQueryString()
{
return queryString;
}
/*******************************************************************************
** Setter for queryString
**
*******************************************************************************/
public void setQueryString(String queryString)
{
this.queryString = queryString;
}
/*******************************************************************************
** Fluent setter for queryString
**
*******************************************************************************/
public APILog withQueryString(String queryString)
{
this.queryString = queryString;
return (this);
}
/*******************************************************************************
** Getter for requestBody
**
*******************************************************************************/
public String getRequestBody()
{
return requestBody;
}
/*******************************************************************************
** Setter for requestBody
**
*******************************************************************************/
public void setRequestBody(String requestBody)
{
this.requestBody = requestBody;
}
/*******************************************************************************
** Fluent setter for requestBody
**
*******************************************************************************/
public APILog withRequestBody(String requestBody)
{
this.requestBody = requestBody;
return (this);
}
/*******************************************************************************
** Getter for responseBody
**
*******************************************************************************/
public String getResponseBody()
{
return responseBody;
}
/*******************************************************************************
** Setter for responseBody
**
*******************************************************************************/
public void setResponseBody(String responseBody)
{
this.responseBody = responseBody;
}
/*******************************************************************************
** Fluent setter for responseBody
**
*******************************************************************************/
public APILog withResponseBody(String responseBody)
{
this.responseBody = responseBody;
return (this);
}
}

View File

@ -0,0 +1,119 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.api.model.APILog;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
/*******************************************************************************
**
*******************************************************************************/
public class APILogMetaDataProvider
{
/*******************************************************************************
**
*******************************************************************************/
public static void defineAll(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
defineAPILogTable(qInstance, backendName, backendDetailEnricher);
}
/*******************************************************************************
**
*******************************************************************************/
private static void defineAPILogTable(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName("apiLog")
.withLabel("API Log")
.withIcon(new QIcon().withName("data_object"))
.withBackendName(backendName)
.withRecordLabelFormat("%s")
.withPrimaryKeyField("id")
.withFieldsFromEntity(APILog.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id")))
.withSection(new QFieldSection("request", new QIcon().withName("arrow_upward"), Tier.T2, List.of("method", "version", "path", "queryString", "requestBody")))
.withSection(new QFieldSection("response", new QIcon().withName("arrow_downward"), Tier.T2, List.of("statusCode", "responseBody")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("timestamp")))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
tableMetaData.getField("requestBody").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json")));
tableMetaData.getField("responseBody").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json")));
tableMetaData.getField("method").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP)
.withValue(AdornmentType.ChipValues.colorValue("GET", AdornmentType.ChipValues.COLOR_INFO))
.withValue(AdornmentType.ChipValues.colorValue("POST", AdornmentType.ChipValues.COLOR_SUCCESS))
.withValue(AdornmentType.ChipValues.colorValue("DELETE", AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue("PATCH", AdornmentType.ChipValues.COLOR_WARNING)));
tableMetaData.getField("statusCode").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP)
.withValue(AdornmentType.ChipValues.colorValue(200, AdornmentType.ChipValues.COLOR_SUCCESS))
.withValue(AdornmentType.ChipValues.colorValue(201, AdornmentType.ChipValues.COLOR_SUCCESS))
.withValue(AdornmentType.ChipValues.colorValue(204, AdornmentType.ChipValues.COLOR_SUCCESS))
.withValue(AdornmentType.ChipValues.colorValue(207, AdornmentType.ChipValues.COLOR_INFO))
.withValue(AdornmentType.ChipValues.colorValue(400, AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue(401, AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue(403, AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue(404, AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue(429, AdornmentType.ChipValues.COLOR_ERROR))
.withValue(AdornmentType.ChipValues.colorValue(500, AdornmentType.ChipValues.COLOR_ERROR)));
///////////////////////////////////////////
// these are the lengths of a MySQL TEXT //
///////////////////////////////////////////
tableMetaData.getField("requestBody").withMaxLength(65_535).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
tableMetaData.getField("responseBody").withMaxLength(65_535).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
//////////////////////////////////////////////////////////////////////////////////////////////
// internet doesn't agree on max-length for a URL, but let's go with ... 4K on query string //
//////////////////////////////////////////////////////////////////////////////////////////////
tableMetaData.getField("queryString").withMaxLength(4096).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
////////////////////////////////////////
// and we expect short paths, 100 max //
////////////////////////////////////////
tableMetaData.getField("path").withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(tableMetaData);
}
qInstance.addTable(tableMetaData);
}
}