Add outbound api logs. FTW.

This commit is contained in:
2023-04-14 16:29:07 -05:00
parent d760831431
commit 514b70eb88
3 changed files with 521 additions and 0 deletions

View File

@ -26,11 +26,14 @@ import java.io.IOException;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -61,9 +64,12 @@ import com.kingsrook.qqq.backend.module.api.exceptions.OAuthCredentialsException
import com.kingsrook.qqq.backend.module.api.exceptions.OAuthExpiredTokenException;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.OutboundAPILog;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -878,6 +884,8 @@ public class BaseAPIActionUtil
{
QHttpResponse qResponse = new QHttpResponse(response);
logOutboundApiCall(request, qResponse);
int statusCode = qResponse.getStatusCode();
if(statusCode == HttpStatus.SC_TOO_MANY_REQUESTS)
{
@ -942,6 +950,46 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected void logOutboundApiCall(HttpRequestBase request, QHttpResponse response)
{
QTableMetaData table = QContext.getQInstance().getTable(OutboundAPILog.TABLE_NAME);
if(table == null)
{
return;
}
String requestBody = null;
if(request instanceof HttpEntityEnclosingRequest entityRequest)
{
try
{
requestBody = StringUtils.join("\n", IOUtils.readLines(entityRequest.getEntity().getContent()));
}
catch(Exception e)
{
// leave it null...
}
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(List.of(new OutboundAPILog()
.withMethod(request.getMethod())
.withUrl(request.getURI().toString()) // todo - does this have the query string?
.withTimestamp(Instant.now())
.withRequestBody(requestBody)
.withStatusCode(response.getStatusCode())
.withResponseBody(response.getContent())
.toQRecord()
));
new InsertAction().executeAsync(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,320 @@
/*
* 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.backend.module.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;
/*******************************************************************************
** Entity bean for OutboundApiLog table
*******************************************************************************/
public class OutboundAPILog extends QRecordEntity
{
public static final String TABLE_NAME = "outboundApiLog";
@QField(isEditable = false)
private Integer id;
@QField()
private Instant timestamp;
@QField(possibleValueSourceName = "outboundApiMethod")
private String method;
@QField(possibleValueSourceName = "outboundApiStatusCode")
private Integer statusCode;
@QField(label = "URL")
private String url;
@QField()
private String requestBody;
@QField()
private String responseBody;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OutboundAPILog()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OutboundAPILog(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 OutboundAPILog 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 OutboundAPILog 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 OutboundAPILog 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 OutboundAPILog withStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
return (this);
}
/*******************************************************************************
** Getter for url
**
*******************************************************************************/
public String getUrl()
{
return url;
}
/*******************************************************************************
** Setter for url
**
*******************************************************************************/
public void setUrl(String url)
{
this.url = url;
}
/*******************************************************************************
** Fluent setter for url
**
*******************************************************************************/
public OutboundAPILog withUrl(String url)
{
this.url = url;
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 OutboundAPILog 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 OutboundAPILog withResponseBody(String responseBody)
{
this.responseBody = responseBody;
return (this);
}
}

View File

@ -0,0 +1,153 @@
/*
* 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.backend.module.api.model;
import java.util.List;
import java.util.function.Consumer;
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.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
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 OutboundAPILogMetaDataProvider
{
/*******************************************************************************
**
*******************************************************************************/
public static void defineAll(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
definePossibleValueSources(qInstance);
defineOutboundAPILogTable(qInstance, backendName, backendDetailEnricher);
}
/*******************************************************************************
**
*******************************************************************************/
private static void definePossibleValueSources(QInstance instance)
{
instance.addPossibleValueSource(new QPossibleValueSource()
.withName("outboundApiMethod")
.withType(QPossibleValueSourceType.ENUM)
.withEnumValues(List.of(
new QPossibleValue<>("GET"),
new QPossibleValue<>("POST"),
new QPossibleValue<>("PUT"),
new QPossibleValue<>("PATCH"),
new QPossibleValue<>("DELETE")
)));
instance.addPossibleValueSource(new QPossibleValueSource()
.withName("outboundApiStatusCode")
.withType(QPossibleValueSourceType.ENUM)
.withEnumValues(List.of(
new QPossibleValue<>(200, "200 (OK)"),
new QPossibleValue<>(201, "201 (Created)"),
new QPossibleValue<>(204, "204 (No Content)"),
new QPossibleValue<>(207, "207 (Multi-Status)"),
new QPossibleValue<>(400, "400 (Bad Request)"),
new QPossibleValue<>(401, "401 (Not Authorized)"),
new QPossibleValue<>(403, "403 (Forbidden)"),
new QPossibleValue<>(404, "404 (Not Found)"),
new QPossibleValue<>(429, "429 (Too Many Requests)"),
new QPossibleValue<>(500, "500 (Internal Server Error)")
)));
}
/*******************************************************************************
**
*******************************************************************************/
private static void defineOutboundAPILogTable(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName(OutboundAPILog.TABLE_NAME)
.withLabel("Outbound API Log")
.withIcon(new QIcon().withName("data_object"))
.withBackendName(backendName)
.withRecordLabelFormat("%s")
.withPrimaryKeyField("id")
.withFieldsFromEntity(OutboundAPILog.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", "url", "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);
/////////////////////////
// limit url to 250... //
/////////////////////////
tableMetaData.getField("url").withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(tableMetaData);
}
qInstance.addTable(tableMetaData);
}
}