From b203f60774ecd09bfca3cd4e1b8d8d9081144181 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Wed, 18 Sep 2024 10:03:26 -0400 Subject: [PATCH] Resolved #635 by adding a new LoggerRestResource that can be used by external systems to store logging data in Salesforce, using OpenTelemetry's logs data model --- README.md | 3 +- .../apex/Log-Management/LoggerRestResource.md | 193 +++++++ docs/apex/index.md | 4 + .../classes/LogEntryEventHandler.cls | 1 + .../classes/LoggerRestResource.cls | 493 ++++++++++++++++++ .../classes/LoggerRestResource.cls-meta.xml | 5 + .../fields/OriginSystemName__c.field-meta.xml | 14 + .../fields/OriginType__c.field-meta.xml | 14 +- .../AllApiLogEntries.listView-meta.xml | 18 + .../LoggerAdmin.permissionset-meta.xml | 9 + .../LoggerEndUser.permissionset-meta.xml | 5 + .../LoggerLogViewer.permissionset-meta.xml | 5 + ...ggerRestIntegration.permissionset-meta.xml | 10 + .../main/logger-engine/classes/Logger.cls | 2 +- .../lwc/logger/logEntryBuilder.js | 2 +- .../fields/OriginSystemName__c.field-meta.xml | 16 + .../core/tests/LoggerCore.testSuite-meta.xml | 1 + .../classes/LogEntryEventHandler_Tests.cls | 2 + .../classes/LoggerRestResource_Tests.cls | 188 +++++++ .../LoggerRestResource_Tests.cls-meta.xml | 5 + .../classes/ComponentLogger_Tests.cls | 2 +- package.json | 4 +- sfdx-project.json | 6 +- 23 files changed, 989 insertions(+), 13 deletions(-) create mode 100644 docs/apex/Log-Management/LoggerRestResource.md create mode 100644 nebula-logger/core/main/log-management/classes/LoggerRestResource.cls create mode 100644 nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSystemName__c.field-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml create mode 100644 nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml create mode 100644 nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginSystemName__c.field-meta.xml create mode 100644 nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls create mode 100644 nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls-meta.xml diff --git a/README.md b/README.md index 06897fbec..d0807f4ad 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, Process Builder & integrations. -## Unlocked Package - v4.14.10 +## Unlocked Package - v4.14.11 [![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oTdQAI) [![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oTdQAI) @@ -40,6 +40,7 @@ The most robust observability solution for Salesforce experts. Built 100% native - [Lightning Components](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-Components): lightning web components (LWCs) & aura components - [Flow & Process Builder](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-Flow): any Flow type that supports invocable actions - [OmniStudio](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-OmniStudio): omniscripts and omni integration procedures + - [OpenTelemetry (OTel) REST API](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-OpenTelemetry-REST-API): inbound integrations, using HTTP and [OTel's JSON format for logs](https://github.com/open-telemetry/opentelemetry-proto/blob/main/examples/logs.json) 2. Built with an event-driven pub/sub messaging architecture, using `LogEntryEvent__e` [platform events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm). For more details on leveraging platform events, see [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm) diff --git a/docs/apex/Log-Management/LoggerRestResource.md b/docs/apex/Log-Management/LoggerRestResource.md new file mode 100644 index 000000000..17446677f --- /dev/null +++ b/docs/apex/Log-Management/LoggerRestResource.md @@ -0,0 +1,193 @@ +--- +layout: default +--- + +## LoggerRestResource class + +REST Resource class for external integrations to interact with Nebula Logger + +--- + +### Properties + +#### `body` → `String` + +#### `endpointRequest` → `EndpointRequest` + +#### `errors` → `List` + +#### `headerKeys` → `List` + +#### `httpMethod` → `String` + +#### `isSuccess` → `Boolean` + +#### `message` → `String` + +#### `name` → `String` + +#### `parameters` → `Map` + +#### `particle` → `String` + +#### `statusCode` → `Integer` + +#### `type` → `String` + +#### `uri` → `String` + +--- + +### Methods + +#### `EndpointError(System.Exception apexException)` → `public` + +#### `EndpointError(String message)` → `public` + +#### `EndpointError(String message, String type)` → `public` + +#### `EndpointRequest(System.RestRequest restRequest)` → `public` + +#### `addError(System.Exception apexException)` → `EndpointResponse` + +#### `addError(EndpointError endpointError)` → `EndpointResponse` + +#### `handlePost()` → `void` + +Processes any HTTP POST requests sent + +#### `handlePost(EndpointRequest endpointRequest)` → `EndpointResponse` + +#### `handlePost(EndpointRequest endpointRequest)` → `EndpointResponse` + +#### `handlePost(EndpointRequest endpointRequest)` → `EndpointResponse` + +#### `setStatusCode(Integer statusCode)` → `EndpointResponse` + +--- + +### Inner Classes + +#### LoggerRestResource.OTelAttribute class + +--- + +##### Constructors + +###### `OTelAttribute(String key, String value)` + +--- + +##### Properties + +###### `key` → `String` + +###### `value` → `OTelAttributeValue` + +--- + +#### LoggerRestResource.OTelAttributeValue class + +--- + +##### Constructors + +###### `OTelAttributeValue(String value)` + +--- + +##### Properties + +###### `stringValue` → `String` + +--- + +#### LoggerRestResource.OTelLogRecord class + +--- + +##### Properties + +###### `attributes` → `List` + +###### `body` → `OTelAttributeValue` + +###### `severityText` → `String` + +###### `timeUnixNano` → `String` + +--- + +##### Methods + +###### `getLogEntryEvent()` → `LogEntryEvent__e` + +--- + +#### LoggerRestResource.OTelLogsPayload class + +--- + +##### Properties + +###### `resourceLogs` → `List` + +--- + +##### Methods + +###### `getConvertedLogEntryEvents()` → `List` + +--- + +#### LoggerRestResource.OTelResource class + +--- + +##### Properties + +###### `attributes` → `List` + +--- + +#### LoggerRestResource.OTelResourceLog class + +--- + +##### Properties + +###### `resource` → `OTelResource` + +###### `scopeLogs` → `List` + +--- + +##### Methods + +###### `getLogEntryEvents()` → `List` + +--- + +#### LoggerRestResource.OTelScope class + +--- + +##### Properties + +###### `name` → `String` + +###### `version` → `String` + +--- + +#### LoggerRestResource.OTelScopeLog class + +--- + +##### Properties + +###### `logRecords` → `List` + +###### `scope` → `OTelScope` + +--- diff --git a/docs/apex/index.md b/docs/apex/index.md index 93b76c167..3fae8b113 100644 --- a/docs/apex/index.md +++ b/docs/apex/index.md @@ -124,6 +124,10 @@ Builds and sends email notifications when internal exceptions occur within the l Controller class for the LWC `loggerHomeHeader` +### [LoggerRestResource](Log-Management/LoggerRestResource) + +REST Resource class for external integrations to create & retrieve logging data + ### [LoggerSObjectMetadata](Log-Management/LoggerSObjectMetadata) Provides details to LWCs about Logger's `SObjects`, using `@AuraEnabled` properties diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index 676b8c962..decb82a90 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -338,6 +338,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { OriginSourceApiName__c = logEntryEvent.OriginSourceApiName__c, OriginSourceId__c = logEntryEvent.OriginSourceId__c, OriginSourceMetadataType__c = logEntryEvent.OriginSourceMetadataType__c, + OriginSystemName__c = logEntryEvent.OriginSystemName__c, OriginType__c = logEntryEvent.OriginType__c, RecordCollectionSize__c = logEntryEvent.RecordCollectionSize__c, RecordCollectionType__c = logEntryEvent.RecordCollectionType__c, diff --git a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls new file mode 100644 index 000000000..1ef85a132 --- /dev/null +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls @@ -0,0 +1,493 @@ +//------------------------------------------------------------------------------------------------// +// This file is part of the Nebula Logger project, released under the MIT License. // +// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // +//------------------------------------------------------------------------------------------------// + +/** + * @group Log Management + * @description REST Resource class for external integrations to interact with Nebula Logger + */ + +@RestResource(urlMapping='/logger/*') +@SuppressWarnings('PMD.ApexDoc, PMD.AvoidDebugStatements, PMD.AvoidGlobalModifier, PMD.CognitiveComplexity') +global with sharing class LoggerRestResource { + @TestVisible + private static final String REQUEST_URI_BASE = '/logger'; + @TestVisible + private static final Integer STATUS_CODE_200_OK = 200; + @TestVisible + private static final Integer STATUS_CODE_201_CREATED = 201; + @TestVisible + private static final Integer STATUS_CODE_400_BAD_REQUEST = 400; + @TestVisible + private static final Integer STATUS_CODE_401_NOT_AUTHORIZED = 401; + @TestVisible + private static final Integer STATUS_CODE_404_NOT_FOUND = 404; + @TestVisible + private static final Integer STATUS_CODE_405_METHOD_NOT_ALLOWED = 405; + private static final Boolean SUPPRESS_NULLS_IN_JSON_SERIALIZATION = true; + + /** + * @description Processes any HTTP POST requests sent + */ + @HttpPost + global static void handlePost() { + // TODO wrap everything in a try-catch block + EndpointRequest endpointRequest = new EndpointRequest(System.RestContext.request); + Endpoint endpoint = getEndpoint(endpointRequest.name); + + EndpointResponse endpointResponse = endpoint.handlePost(endpointRequest); + System.RestContext.response = buildRestResponse(endpointResponse); + + logErrors(endpointRequest, endpointResponse, System.RestContext.request, System.RestContext.response); + } + + private static Endpoint getEndpoint(String endpointName) { + switch on endpointName { + when 'logs' { + return new LogsEndpoint(); + } + when else { + return new UnknownEndpointResponder(); + } + } + } + + private static System.RestResponse buildRestResponse(EndpointResponse endpointResponse) { + System.RestResponse restResponse = System.RestContext.response ?? new System.RestResponse(); + restResponse.addHeader('Content-Type', 'application/json'); + restResponse.responseBody = Blob.valueOf(System.JSON.serialize(endpointResponse, SUPPRESS_NULLS_IN_JSON_SERIALIZATION)); + restResponse.statusCode = endpointResponse.statusCode; + return restResponse; + } + + // TODO revisit - this is probably too many parameters...? + @SuppressWarnings('PMD.ExcessiveParameterList') + private static void logErrors( + EndpointRequest endpointRequest, + EndpointResponse endpointResponse, + System.RestRequest restRequest, + System.RestResponse restResponse + ) { + if (endpointResponse.isSuccess) { + return; + } + + LogMessage warningMessage = new LogMessage( + 'Inbound call to {0} endpoint failed with {1} errors:\n\n{2}', + REQUEST_URI_BASE + '/' + endpointRequest.name, + endpointResponse.errors.size(), + System.JSON.serializePretty(endpointResponse.errors) + ); + Logger.warn(warningMessage).setRestRequestDetails(restRequest).setRestResponseDetails(restResponse); + Logger.saveLog(); + } + + /* Base classes that act as the building blocks for all endpoints */ + private abstract class Endpoint { + public abstract EndpointResponse handlePost(EndpointRequest endpointRequest); + } + + @TestVisible + private class EndpointRequest { + public String body; + // public EndpointRequestContext context; + public List headerKeys; + public String httpMethod; + public String name; + public Map parameters; + public String particle; + public String uri; + + public EndpointRequest(System.RestRequest restRequest) { + String parsedName = this.getEndpointName(restRequest.requestUri); + String requestBody = restRequest.requestBody?.toString(); + + this.body = String.isBlank(requestBody) ? null : requestBody; + this.headerKeys = new List(restRequest.headers.keySet()); + this.httpMethod = restRequest.httpMethod; + this.name = parsedName; + this.parameters = restRequest.params; + this.particle = this.getEndpointParticle(restRequest.requestUri, parsedName); + this.uri = restRequest.requestUri; + } + + private String getEndpointName(String restRequestUri) { + // FIXME the comments below are no longer accurate - endpoints like /logs/ are now used + /* + Endpoint names will (at least for now) only have one layer, using formats like: + /logger/logs + /logger/logs/?some-url-parameter=true&and-another=true + /logger/something + /logger/something?another-url-parameter=something + /Nebula/logger/logs + /Nebula/logger/logs/?some-url-parameter=true&and-another=true + /Nebula/logger/something + /Nebula/logger/something?another-url-parameter=something + + The endpoint name will be just the last bit of the URL, without any parameters or '/' slashes. + So if the URL is: + /logger/something?some-url-parameter=true&and-another=true + then the endpoint name will be 'something' + */ + + String parsedEndpointName = restRequestUri.substringAfter(REQUEST_URI_BASE); + if (parsedEndpointName.contains('?')) { + parsedEndpointName = parsedEndpointName.substringBefore('?'); + } + parsedEndpointName = parsedEndpointName.removeStart('/').removeEnd('/'); + if (parsedEndpointName.contains('/')) { + parsedEndpointName = parsedEndpointName.substringBefore('/'); + } + return String.isNotBlank(parsedEndpointName) ? parsedEndpointName : null; + } + + private String getEndpointParticle(String restRequestUri, String endpointName) { + String parsedEndpointParticle = restRequestUri.substringAfter('/' + endpointName + '/'); + if (parsedEndpointParticle?.contains('?')) { + parsedEndpointParticle = parsedEndpointParticle.substringBefore('?'); + } + parsedEndpointParticle = parsedEndpointParticle.removeEnd('/'); + + return String.isBlank(parsedEndpointParticle) ? null : parsedEndpointParticle; + } + } + + @TestVisible + private class EndpointResponse { + public final List errors = new List(); + + // The status code doesn't need to be returned in the RestResponse body + // since the RestResponse headers will include the status code, so use + // 'transient' to exclude it during serialization + public transient Integer statusCode; + + public Boolean isSuccess { + get { + return this.errors.isEmpty(); + } + } + + public EndpointResponse addError(System.Exception apexException) { + return this.addError(new EndpointError(apexException)); + } + + public EndpointResponse addError(EndpointError endpointError) { + this.errors.add(endpointError); + return this; + } + + public EndpointResponse setStatusCode(Integer statusCode) { + this.statusCode = statusCode; + return this; + } + } + + @TestVisible + private virtual class EndpointError { + public final String message; + public final String type; + + public EndpointError(System.Exception apexException) { + this(apexException.getMessage(), apexException.getTypeName()); + } + + public EndpointError(String message) { + this(message, null); + } + + public EndpointError(String message, String type) { + this.message = message; + this.type = type; + } + } + + /* Endpoint implementations */ + private class LogsEndpoint extends Endpoint { + public override EndpointResponse handlePost(EndpointRequest endpointRequest) { + EndpointResponse postResponse = new EndpointResponse(); + try { + OTelLogsPayload logsPayload = this.deserializeLog(endpointRequest.body); + this.saveLog(logsPayload); + postResponse.setStatusCode(STATUS_CODE_201_CREATED); + return postResponse; + } catch (Exception apexException) { + postResponse.setStatusCode(STATUS_CODE_400_BAD_REQUEST).addError(apexException); + return postResponse; + } + } + private void saveLog(OTelLogsPayload logsPayload) { + LoggerDataStore.getEventBus().publishRecords(logsPayload.getConvertedLogEntryEvents()); + } + + private OTelLogsPayload deserializeLog(String jsonBody) { + if (String.isBlank(jsonBody)) { + throw new System.IllegalArgumentException('No data provided'); + } + + return (OTelLogsPayload) System.JSON.deserialize(jsonBody, OTelLogsPayload.class); + } + } + + private class UnknownEndpointResponder extends Endpoint { + public override EndpointResponse handlePost(EndpointRequest endpointRequest) { + return this.handleResponse(endpointRequest); + } + + private EndpointResponse handleResponse(EndpointRequest endpointRequest) { + String errorMessage = 'Calling root endpoint /logger is not supported, please provide a specific endpoint'; + if (endpointRequest.name != null) { + errorMessage = 'Unknown endpoint provided: ' + endpointRequest.name; + } + return new EndpointResponse().setStatusCode(STATUS_CODE_404_NOT_FOUND).addError(new EndpointError(errorMessage)); + } + } + + // OpenTelemetry classes - these correspond to OTel v1.36.0's HTTP JSON format for the logs data model + // https://opentelemetry.io/docs/specs/otel/logs/data-model/ + // https://opentelemetry.io/docs/specs/otel/protocol/file-exporter/#examples + // https://github.com/open-telemetry/opentelemetry-proto/blob/main/examples/logs.json + public class OTelLogsPayload { + public final List resourceLogs = new List(); + + public List getConvertedLogEntryEvents() { + List logEntryEvents = new List(); + + for (OTelResourceLog resourceLog : this.resourceLogs) { + logEntryEvents.addAll(resourceLog.getLogEntryEvents()); + } + + return logEntryEvents; + } + } + + public class OTelResourceLog { + public final OTelResource resource = new OTelResource(); + public final List scopeLogs = new List(); + + public List getLogEntryEvents() { + List logEntryEvents = new List(); + + for (OTelScopeLog scopeLog : this.scopeLogs) { + for (OTelLogRecord otelLogEntry : scopeLog.logRecords) { + LogEntryEvent__e convertedLogEntryEvent = otelLogEntry.getLogEntryEvent(); + Map supplementalFieldToValue = this.resource.convertAttributes(); + for (Schema.SObjectField field : supplementalFieldToValue.keySet()) { + convertedLogEntryEvent.put(field, supplementalFieldToValue.get(field)); + } + logEntryEvents.add(convertedLogEntryEvent); + } + } + + return logEntryEvents; + } + } + + // OTel supports additional types boolValue, float64Value, and intValue + // but there's not currently a need for them in Nebula Logger's data model + // As more mappings are added, these types will be re-added when needed + public class OTelAttribute { + public final String key; + public final OTelAttributeValue value; + + // public OTelAttribute(String key, Boolean value) { + // this.key = key; + // this.value = new OTelAttributeValue(value); + // } + + // public OTelAttribute(String key, Decimal value) { + // this.key = key; + // this.value = new OTelAttributeValue(value); + // } + + // public OTelAttribute(String key, Integer value) { + // this.key = key; + // this.value = new OTelAttributeValue(value); + // } + + public OTelAttribute(String key, String value) { + this.key = key; + this.value = new OTelAttributeValue(value); + } + } + + public class OTelAttributeValue { + // public final Boolean boolValue; + // public final Decimal float64Value; + // public final Integer intValue; + public final String stringValue; + + // public OTelAttributeValue(Boolean value) { + // this.boolValue = value; + // } + + // public OTelAttributeValue(Decimal value) { + // this.float64Value = value; + // } + + // public OTelAttributeValue(Integer value) { + // this.intValue = value; + // } + + public OTelAttributeValue(String value) { + this.stringValue = value; + } + } + + public class OTelResource { + public List attributes = new List(); + + private Map convertAttributes() { + Map supplementalFieldToValue = new Map(); + + for (OTelAttribute entryAttribute : this.attributes) { + switch on entryAttribute.key { + when 'service.name' { + supplementalFieldToValue.put(LogEntryEvent__e.OriginSystemName__c, entryAttribute.value?.stringValue); + } + // TODO + // when 'service.version' { + // } + } + } + + return supplementalFieldToValue; + } + } + + public class OTelScope { + public String name; + public String version; + } + + public class OTelScopeLog { + public OTelScope scope; + public List logRecords = new List(); + } + + public class OTelLogRecord { + public List attributes = new List(); + public OTelAttributeValue body; + public String severityText; + public String timeUnixNano = (System.now().getTime() * 1000000).toString(); + // TODO revisit mappings for spanId and traceId + // public String spanId; + // public String traceId; + + private transient LogEntryEvent__e convertedLogEntryEvent; + + public LogEntryEvent__e getLogEntryEvent() { + if (this.convertedLogEntryEvent == null) { + System.LoggingLevel entryLoggingLevel = this.getLoggingLevel(); + Long entryEpochTimestamp = Long.valueOf(this.timeUnixNano) / 1000000; + Datetime entryTimestamp = Datetime.newInstance(entryEpochTimestamp); + + this.convertedLogEntryEvent = Logger.newEntry(entryLoggingLevel, this.body?.stringValue).setTimestamp(entryTimestamp).getLogEntryEvent(); + // Since the log entries originate off-platform, tracking the limits usage isn't really relevant here + this.convertedLogEntryEvent.LimitsAggregateQueriesMax__c = null; + this.convertedLogEntryEvent.LimitsAggregateQueriesUsed__c = null; + this.convertedLogEntryEvent.LimitsAsyncCallsMax__c = null; + this.convertedLogEntryEvent.LimitsAsyncCallsUsed__c = null; + this.convertedLogEntryEvent.LimitsCalloutsMax__c = null; + this.convertedLogEntryEvent.LimitsCalloutsUsed__c = null; + this.convertedLogEntryEvent.LimitsCpuTimeMax__c = null; + this.convertedLogEntryEvent.LimitsCpuTimeUsed__c = null; + this.convertedLogEntryEvent.LimitsDmlRowsMax__c = null; + this.convertedLogEntryEvent.LimitsDmlRowsUsed__c = null; + this.convertedLogEntryEvent.LimitsDmlStatementsMax__c = null; + this.convertedLogEntryEvent.LimitsDmlStatementsUsed__c = null; + this.convertedLogEntryEvent.LimitsEmailInvocationsMax__c = null; + this.convertedLogEntryEvent.LimitsEmailInvocationsUsed__c = null; + this.convertedLogEntryEvent.LimitsFutureCallsMax__c = null; + this.convertedLogEntryEvent.LimitsFutureCallsUsed__c = null; + this.convertedLogEntryEvent.LimitsHeapSizeMax__c = null; + this.convertedLogEntryEvent.LimitsHeapSizeUsed__c = null; + this.convertedLogEntryEvent.LimitsMobilePushApexCallsMax__c = null; + this.convertedLogEntryEvent.LimitsMobilePushApexCallsUsed__c = null; + this.convertedLogEntryEvent.LimitsPublishImmediateDmlStatementsMax__c = null; + this.convertedLogEntryEvent.LimitsPublishImmediateDmlStatementsUsed__c = null; + this.convertedLogEntryEvent.LimitsQueueableJobsMax__c = null; + this.convertedLogEntryEvent.LimitsQueueableJobsUsed__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueriesMax__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueriesUsed__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueryLocatorRowsMax__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueryLocatorRowsUsed__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueryRowsMax__c = null; + this.convertedLogEntryEvent.LimitsSoqlQueryRowsUsed__c = null; + this.convertedLogEntryEvent.LimitsSoslSearchesMax__c = null; + this.convertedLogEntryEvent.LimitsSoslSearchesUsed__c = null; + this.convertedLogEntryEvent.OriginLocation__c = null; + this.convertedLogEntryEvent.OriginSourceActionName__c = null; + this.convertedLogEntryEvent.OriginSourceApiName__c = null; + this.convertedLogEntryEvent.OriginSourceId__c = null; + this.convertedLogEntryEvent.OriginSourceMetadataType__c = null; + this.convertedLogEntryEvent.OriginType__c = 'API'; + this.convertedLogEntryEvent.StackTrace__c = null; + + Map supplementalFieldToValue = this.convertAttributes(); + for (Schema.SObjectField field : supplementalFieldToValue.keySet()) { + this.convertedLogEntryEvent.put(field, supplementalFieldToValue.get(field)); + } + } + + return this.convertedLogEntryEvent; + } + + private System.LoggingLevel getLoggingLevel() { + switch on this.severityText?.toLowerCase() { + when 'error' { + return System.LoggingLevel.ERROR; + } + when 'warn' { + return System.LoggingLevel.WARN; + } + when 'info' { + return System.LoggingLevel.INFO; + } + when 'debug' { + return System.LoggingLevel.DEBUG; + } + when 'trace3' { + return System.LoggingLevel.FINE; + } + when 'trace2' { + return System.LoggingLevel.FINER; + } + when 'trace' { + return System.LoggingLevel.FINEST; + } + when else { + // Use DEBUG as a fallback value, similar to how it's done in Logger + System.debug(System.LoggingLevel.DEBUG, 'Unable to convert severity text to logging level: ' + this.severityText); + return System.LoggingLevel.DEBUG; + } + } + } + + private Map convertAttributes() { + Map supplementalFieldToValue = new Map(); + + for (OTelAttribute entryAttribute : this.attributes) { + switch on entryAttribute.key { + when 'exception.message' { + supplementalFieldToValue.put(LogEntryEvent__e.ExceptionMessage__c, entryAttribute.value?.stringValue); + } + when 'exception.stack_trace' { + supplementalFieldToValue.put(LogEntryEvent__e.ExceptionStackTrace__c, entryAttribute.value?.stringValue); + } + when 'exception.type' { + supplementalFieldToValue.put(LogEntryEvent__e.ExceptionType__c, entryAttribute.value?.stringValue); + } + when 'origin.stack_trace' { + supplementalFieldToValue.put(LogEntryEvent__e.StackTrace__c, entryAttribute.value?.stringValue); + } + when 'parent_log.transaction_id' { + supplementalFieldToValue.put(LogEntryEvent__e.ParentLogTransactionId__c, entryAttribute.value?.stringValue); + } + } + } + + return supplementalFieldToValue; + } + } +} diff --git a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml new file mode 100644 index 000000000..c01f6433a --- /dev/null +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSystemName__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSystemName__c.field-meta.xml new file mode 100644 index 000000000..895fe3ea6 --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSystemName__c.field-meta.xml @@ -0,0 +1,14 @@ + + + OriginSystemName__c + Active + None + true + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml index 94c54452f..0cf6fcf4f 100644 --- a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml @@ -13,25 +13,31 @@ false Apex - #333333 + #FF595E false + + API + #FFCA3A + false + + Component - #A845DC + #8AC926 false Flow - #FFCC33 + #1982C4 false OmniStudio - #FFCC33 + #6A4C93 false diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml new file mode 100644 index 000000000..beef641cb --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml @@ -0,0 +1,18 @@ + + + AllApiLogEntries + NAME + Log__c + LoggingLevel__c + LoggedByUsernameLink__c + Message__c + OriginSystemName__c + Timestamp__c + Everything + + OriginType__c + equals + API + + + diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml index aa85c701f..c342a83d5 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml @@ -64,6 +64,10 @@ Logger true + + LoggerRestResource + true + LoggerHomeHeaderController true @@ -976,6 +980,11 @@ LogEntry__c.OriginSourceSnippet__c true + + false + LogEntry__c.OriginSystemName__c + true + false LogEntry__c.OriginType__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml index cdaad82f0..c6619df2d 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml @@ -607,6 +607,11 @@ LogEntry__c.OriginSourceMetadataType__c true + + false + LogEntry__c.OriginSystemName__c + true + false LogEntry__c.OriginType__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml index f226455a8..b94255ba7 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml @@ -896,6 +896,11 @@ LogEntry__c.OriginSourceSnippet__c true + + false + LogEntry__c.OriginSystemName__c + true + false LogEntry__c.OriginType__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml new file mode 100644 index 000000000..3c5876720 --- /dev/null +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml @@ -0,0 +1,10 @@ + + + + LoggerRestResource + true + + Provides access to integrate with Nebula Logger via REST API calls + false + + diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 9fa54941a..2d0b82c62 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -15,7 +15,7 @@ global with sharing class Logger { // There's no reliable way to get the version number dynamically in Apex @TestVisible - private static final String CURRENT_VERSION_NUMBER = 'v4.14.10'; + private static final String CURRENT_VERSION_NUMBER = 'v4.14.11'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final List LOG_ENTRIES_BUFFER = new List(); private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.'; diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js index 1150bda94..2a964b65b 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js @@ -6,7 +6,7 @@ import FORM_FACTOR from '@salesforce/client/formFactor'; import { log as lightningLog } from 'lightning/logger'; import { LoggerStackTrace } from './loggerStackTrace'; -const CURRENT_VERSION_NUMBER = 'v4.14.10'; +const CURRENT_VERSION_NUMBER = 'v4.14.11'; const LOGGING_LEVEL_EMOJIS = { ERROR: '⛔', diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginSystemName__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginSystemName__c.field-meta.xml new file mode 100644 index 000000000..17f4fe7a6 --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginSystemName__c.field-meta.xml @@ -0,0 +1,16 @@ + + + OriginSystemName__c + Active + None + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml b/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml index e456428ab..3e29af43c 100644 --- a/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml +++ b/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml @@ -24,6 +24,7 @@ LoggerHomeHeaderController_Tests LoggerParameter_Tests LoggerPlugin_Tests + LoggerRestResource_Tests LoggerScenarioHandler_Tests LoggerScenarioRule_Tests LoggerSettingsController_Tests diff --git a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls index fa4e48474..2781518ab 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -1439,6 +1439,7 @@ private class LogEntryEventHandler_Tests { OriginSourceApiName__c, OriginSourceId__c, OriginSourceMetadataType__c, + OriginSystemName__c, OriginType__c, RecordCollectionSize__c, RecordCollectionType__c, @@ -1755,6 +1756,7 @@ private class LogEntryEventHandler_Tests { logEntry.OriginSourceMetadataType__c, 'logEntry.OriginSourceMetadataType__c was not properly set' ); + System.Assert.areEqual(logEntryEvent.OriginSystemName__c, logEntry.OriginSystemName__c, 'logEntry.OriginSystemName__c was not properly set'); System.Assert.areEqual(logEntryEvent.OriginType__c, logEntry.OriginType__c, 'logEntry.OriginType__c was not properly set'); System.Assert.areEqual(logEntryEvent.RecordCollectionSize__c, logEntry.RecordCollectionSize__c, 'logEntry.RecordCollectionSize__c was not properly set'); System.Assert.areEqual(logEntryEvent.RecordCollectionType__c, logEntry.RecordCollectionType__c, 'logEntry.RecordCollectionType__c was not properly set'); diff --git a/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls new file mode 100644 index 000000000..d1aee01bf --- /dev/null +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls @@ -0,0 +1,188 @@ +//------------------------------------------------------------------------------------------------// +// This file is part of the Nebula Logger project, released under the MIT License. // +// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // +//------------------------------------------------------------------------------------------------// + +@SuppressWarnings('PMD.ApexDoc, PMD.MethodNamingConventions, PMD.NcssMethodCount') +@IsTest(IsParallel=true) +private class LoggerRestResource_Tests { + @IsTest + static void endpoint_request_correctly_parses_system_rest_request_without_endpoint_particle() { + String expectedEndpointName = 'some-endpoint-name'; + String expectedRequestBody = 'some string that may or may not be valid JSON (but hopefully it is)'; + System.RestRequest restRequest = new System.RestRequest(); + restRequest.addHeader('X-some-header', 'some-value'); + restRequest.addHeader('X-another-header', 'another-value'); + restRequest.addParameter('verbose', 'true'); + restRequest.addParameter('some-other-parameter', 'someValue'); + restRequest.requestBody = Blob.valueOf(expectedRequestBody); + restRequest.requestUri = LoggerRestResource.REQUEST_URI_BASE + '/' + expectedEndpointName + '/'; + + LoggerRestResource.EndpointRequest endpointRequest = new LoggerRestResource.EndpointRequest(restRequest); + + System.Assert.areEqual(expectedRequestBody, endpointRequest.body); + System.Assert.areEqual(expectedEndpointName, endpointRequest.name); + System.Assert.isNull(endpointRequest.particle); + System.Assert.areEqual(new List(restRequest.headers.keySet()), endpointRequest.headerKeys); + System.Assert.areEqual(restRequest.params, endpointRequest.parameters); + System.Assert.areEqual(restRequest.requestUri, endpointRequest.uri); + } + + @IsTest + static void endpoint_request_correctly_parses_system_rest_request_with_endpoint_particle() { + String expectedEndpointName = 'some-endpoint-name'; + String expectedEndpointParticle = System.UUID.randomUUID().toString(); + String expectedRequestBody = 'some string that may or may not be valid JSON (but hopefully it is)'; + System.RestRequest restRequest = new System.RestRequest(); + restRequest.addHeader('X-some-header', 'some-value'); + restRequest.addHeader('X-another-header', 'another-value'); + restRequest.addParameter('verbose', 'true'); + restRequest.addParameter('some-other-parameter', 'someValue'); + restRequest.requestBody = Blob.valueOf(expectedRequestBody); + restRequest.requestUri = LoggerRestResource.REQUEST_URI_BASE + '/' + expectedEndpointName + '/' + expectedEndpointParticle; + + LoggerRestResource.EndpointRequest endpointRequest = new LoggerRestResource.EndpointRequest(restRequest); + + System.Assert.areEqual(expectedRequestBody, endpointRequest.body); + System.Assert.areEqual(expectedEndpointName, endpointRequest.name); + System.Assert.areEqual(expectedEndpointParticle, endpointRequest.particle); + System.Assert.areEqual(new List(restRequest.headers.keySet()), endpointRequest.headerKeys); + System.Assert.areEqual(restRequest.params, endpointRequest.parameters); + System.Assert.areEqual(restRequest.requestUri, endpointRequest.uri); + } + + @IsTest + static void unknown_endpoint_post_throws_an_exception() { + String unknownEndpoint = 'some-endpoint-that-definitely-should-not-exist'; + String someParameters = '/?i-hope=true'; + System.RestContext.request = new System.RestRequest(); + System.RestContext.request.requestUri = LoggerRestResource.REQUEST_URI_BASE + '/' + unknownEndpoint + someParameters; + + LoggerRestResource.handlePost(); + + System.Assert.areEqual(404, System.RestContext.response.statusCode); + System.Assert.areEqual('application/json', System.RestContext.response.headers.get('Content-Type')); + System.Assert.isNotNull(System.RestContext.response.responseBody); + LoggerRestResource.EndpointResponse endpointResponse = (LoggerRestResource.EndpointResponse) System.JSON.deserialize( + System.RestContext.response.responseBody.toString(), + LoggerRestResource.EndpointResponse.class + ); + System.Assert.isFalse(endpointResponse.isSuccess); + System.Assert.areEqual(1, endpointResponse.errors.size()); + System.Assert.areEqual('Unknown endpoint provided: ' + unknownEndpoint, endpointResponse.errors.get(0).message); + } + + @IsTest + static void otel_severity_text_correctly_maps_to_logging_level() { + Map otelSeverityTextToExpectedLoggingLevel = new Map{ + 'Error' => System.LoggingLevel.ERROR, + 'Warn' => System.LoggingLevel.WARN, + 'Info' => System.LoggingLevel.INFO, + 'Debug' => System.LoggingLevel.DEBUG, + 'Trace3' => System.LoggingLevel.FINE, + 'Trace2' => System.LoggingLevel.FINER, + 'Trace' => System.LoggingLevel.FINEST, + 'Anything else' => System.LoggingLevel.DEBUG + }; + for (String otelSeverityText : otelSeverityTextToExpectedLoggingLevel.keySet()) { + LoggerRestResource.OTelLogRecord otelLogEntry = new LoggerRestResource.OTelLogRecord(); + + otelLogEntry.severityText = otelSeverityText; + + System.LoggingLevel expectedLoggingLevel = otelSeverityTextToExpectedLoggingLevel.get(otelSeverityText); + System.Assert.areEqual(expectedLoggingLevel.name(), otelLogEntry.getLogEntryEvent().LoggingLevel__c); + System.Assert.areEqual(expectedLoggingLevel.ordinal(), otelLogEntry.getLogEntryEvent().LoggingLevelOrdinal__c); + } + } + + @IsTest + static void logs_endpoint_post_throws_an_exception_when_null_log_entries_list_is_provided() { + System.RestContext.request = new System.RestRequest(); + System.RestContext.request.requestBody = null; + System.RestContext.request.requestUri = LoggerRestResource.REQUEST_URI_BASE + '/logs'; + + LoggerRestResource.handlePost(); + + System.Assert.areEqual(LoggerRestResource.STATUS_CODE_400_BAD_REQUEST, System.RestContext.response.statusCode); + System.Assert.areEqual('application/json', System.RestContext.response.headers.get('Content-Type')); + System.Assert.isNotNull(System.RestContext.response.responseBody); + LoggerRestResource.EndpointResponse endpointResponse = (LoggerRestResource.EndpointResponse) System.JSON.deserialize( + System.RestContext.response.responseBody.toString(), + LoggerRestResource.EndpointResponse.class + ); + System.Assert.isFalse(endpointResponse.isSuccess); + System.Assert.areEqual(1, endpointResponse.errors.size()); + System.Assert.areEqual('No data provided', endpointResponse.errors.get(0).message); + System.Assert.areEqual(System.IllegalArgumentException.class.getName(), endpointResponse.errors.get(0).type); + } + + @IsTest + static void logs_endpoint_post_successsfully_saves_otel_log_when_data_is_provided() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishCallCount()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + LoggerRestResource.OTelLogRecord otelLogEntry = new LoggerRestResource.OTelLogRecord(); + otelLogEntry.body = new LoggerRestResource.OTelAttributeValue('some message'); + otelLogEntry.severityText = 'Info'; + otelLogEntry.timeUnixNano = '1581452773000000789'; + LoggerRestResource.OTelAttribute exceptionMessageAttribute = new LoggerRestResource.OTelAttribute('exception.message', 'some exception message'); + otelLogEntry.attributes.add(exceptionMessageAttribute); + LoggerRestResource.OTelAttribute exceptionStackTraceAttribute = new LoggerRestResource.OTelAttribute('exception.stack_trace', 'Some.Exception.stackTrace'); + otelLogEntry.attributes.add(exceptionStackTraceAttribute); + LoggerRestResource.OTelAttribute exceptionTypeAttribute = new LoggerRestResource.OTelAttribute('exception.type', 'SomeExceptionType'); + otelLogEntry.attributes.add(exceptionTypeAttribute); + LoggerRestResource.OTelAttribute originStackTraceAttribute = new LoggerRestResource.OTelAttribute('origin.stack_trace', 'Some.Origin.stackTrace'); + otelLogEntry.attributes.add(originStackTraceAttribute); + LoggerRestResource.OTelAttribute parentLogTransactionIdAttribute = new LoggerRestResource.OTelAttribute('parent_log.transaction_id', '123-abc'); + otelLogEntry.attributes.add(parentLogTransactionIdAttribute); + LoggerRestResource.OTelScopeLog scopeLog = new LoggerRestResource.OTelScopeLog(); + scopeLog.logRecords.add(otelLogEntry); + LoggerRestResource.OTelResourceLog resourceLog = new LoggerRestResource.OTelResourceLog(); + LoggerRestResource.OTelAttribute resourceServiceNameAttribute = new LoggerRestResource.OTelAttribute( + 'service.name', + 'some-external-system-or-microservice' + ); + resourceLog.resource.attributes.add(resourceServiceNameAttribute); + resourceLog.scopeLogs.add(scopeLog); + LoggerRestResource.OTelLogsPayload logsPayload = new LoggerRestResource.OTelLogsPayload(); + logsPayload.resourceLogs.add(resourceLog); + System.RestContext.request = new System.RestRequest(); + System.RestContext.request.requestBody = Blob.valueOf(System.JSON.serialize(logsPayload)); + System.RestContext.request.requestUri = LoggerRestResource.REQUEST_URI_BASE + '/logs'; + + LoggerRestResource.handlePost(); + + System.Assert.areEqual( + LoggerRestResource.STATUS_CODE_201_CREATED, + System.RestContext.response.statusCode, + System.RestContext.response.responseBody.toString() + ); + System.Assert.areEqual('application/json', System.RestContext.response.headers.get('Content-Type')); + System.Assert.isNotNull(System.RestContext.response.responseBody); + LoggerRestResource.EndpointResponse endpointResponse = (LoggerRestResource.EndpointResponse) System.JSON.deserialize( + System.RestContext.response.responseBody.toString(), + LoggerRestResource.EndpointResponse.class + ); + System.Assert.isTrue(endpointResponse.isSuccess); + System.Assert.areEqual(0, endpointResponse.errors.size()); + System.Assert.areEqual(1, LoggerMockDataStore.getEventBus().getPublishCallCount()); + System.Assert.areEqual(1, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + LogEntryEvent__e publishedLogEntryEvent = (LogEntryEvent__e) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().get(0); + System.Assert.isNull(publishedLogEntryEvent.OriginLocation__c); + System.Assert.isNull(publishedLogEntryEvent.OriginSourceActionName__c); + System.Assert.isNull(publishedLogEntryEvent.OriginSourceApiName__c); + System.Assert.isNull(publishedLogEntryEvent.OriginSourceId__c); + System.Assert.isNull(publishedLogEntryEvent.OriginSourceMetadataType__c); + System.Assert.areEqual(resourceServiceNameAttribute.value.stringValue, publishedLogEntryEvent.OriginSystemName__c); + System.Assert.areEqual('API', publishedLogEntryEvent.OriginType__c); + System.Assert.areEqual(otelLogEntry.severityText.toUpperCase(), publishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(otelLogEntry.body.stringValue, publishedLogEntryEvent.Message__c); + System.Assert.areEqual(exceptionMessageAttribute.value.stringValue, publishedLogEntryEvent.ExceptionMessage__c); + System.Assert.areEqual(exceptionStackTraceAttribute.value.stringValue, publishedLogEntryEvent.ExceptionStackTrace__c); + System.Assert.areEqual(exceptionTypeAttribute.value.stringValue, publishedLogEntryEvent.ExceptionType__c); + System.Assert.areEqual(parentLogTransactionIdAttribute.value.stringValue, publishedLogEntryEvent.ParentLogTransactionId__c); + System.Assert.areEqual(originStackTraceAttribute.value.stringValue, publishedLogEntryEvent.StackTrace__c); + Datetime expectedTimestamp = Datetime.newInstance(Long.valueOf(otelLogEntry.timeUnixNano) / 1000000); + System.Assert.areEqual(expectedTimestamp, publishedLogEntryEvent.Timestamp__c); + } +} diff --git a/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls-meta.xml b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls-meta.xml new file mode 100644 index 000000000..c01f6433a --- /dev/null +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + diff --git a/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls index adc36a1ac..6e5b6295d 100644 --- a/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls @@ -69,7 +69,7 @@ private class ComponentLogger_Tests { System.Assert.areEqual('Component', publishedLogEntryEvent.OriginType__c); System.Assert.isNull( publishedLogEntryEvent.OriginSourceMetadataType__c, - 'Non-null value populated for OriginSourceMetadata__c: ' + System.JSON.serializePretty(publishedLogEntryEvent) + 'Non-null value populated for OriginSourceMetadataType__c: ' + System.JSON.serializePretty(publishedLogEntryEvent) ); System.Assert.isNull(publishedLogEntryEvent.StackTrace__c); System.Assert.areEqual(componentLogEntry.loggingLevel, publishedLogEntryEvent.LoggingLevel__c); diff --git a/package.json b/package.json index 4aaf72cdf..03763b198 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.14.10", + "version": "4.14.11", "description": "The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects.", "author": "Jonathan Gillespie", "license": "MIT", @@ -58,7 +58,7 @@ "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", "scan:fix:lwc": "eslint --config ./config/linters/.eslintrc.json **/lwc/** --fix", "scan": "npm run scan:apex && npm run scan:lwc", - "scan:apex": "sf scanner:run --pmdconfig ./config/linters/pmd-ruleset.xml --target ./nebula-logger/ --engine pmd --severity-threshold 3", + "scan:apex": "sf scanner run --pmdconfig ./config/linters/pmd-ruleset.xml --target ./nebula-logger/ --engine pmd --severity-threshold 3", "scan:lwc": "eslint --config ./config/linters/.eslintrc.json **/lwc/**", "sf:plugins:link:bummer": "npx sf plugins link ./node_modules/@jongpie/sfdx-bummer-plugin", "sf:plugins:link:prettier": "npx sf plugins link ./node_modules/@jayree/sfdx-plugin-prettier", diff --git a/sfdx-project.json b/sfdx-project.json index 0169bbefc..d58b27f84 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -9,9 +9,9 @@ "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.14.10.NEXT", - "versionName": "New CallableLogger Apex class", - "versionDescription": "Added a new CallableLogger class that provides support for both OmniStudio logging, as well as the ability to dynamically call Nebula Logger in Apex when it's available", + "versionNumber": "4.14.11.NEXT", + "versionName": "OpenTelemetry (OTel) REST Resource", + "versionDescription": "Added a new LoggerRestResource class that provides an OTel-compatible endpoint for external integrations to store logging data in Salesforce", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests"