From 514b70eb88cc1a607b03efc3cb9b16060cd24c73 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 14 Apr 2023 16:29:07 -0500 Subject: [PATCH] Add outbound api logs. FTW. --- .../module/api/actions/BaseAPIActionUtil.java | 48 +++ .../module/api/model/OutboundAPILog.java | 320 ++++++++++++++++++ .../model/OutboundAPILogMetaDataProvider.java | 153 +++++++++ 3 files changed, 521 insertions(+) create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILog.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index 0a8e705b..d8ecf17a 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILog.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILog.java new file mode 100644 index 00000000..7dc460bd --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILog.java @@ -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 . + */ + +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); + } + +} + diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java new file mode 100644 index 00000000..7cb7e062 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java @@ -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 . + */ + +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 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 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); + } +}