diff --git a/README.md b/README.md index 7f8ff2f76..9ea389404 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations. -## Unlocked Package - v4.15.3 +## Unlocked Package - v4.15.4 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015ok2QAA) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015ok2QAA) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015okMQAQ) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015okMQAQ) [![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki) -`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015ok2QAA` +`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015okMQAQ` --- @@ -36,6 +36,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 13d2abca0..e01ec4f30 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.package.xml b/nebula-logger/core.package.xml index 4b740d65d..ed90dc833 100644 --- a/nebula-logger/core.package.xml +++ b/nebula-logger/core.package.xml @@ -70,6 +70,8 @@ LoggerParameter_Tests LoggerPlugin LoggerPlugin_Tests + LoggerRestResource + LoggerRestResource_Tests LoggerSObjectHandler LoggerSObjectHandler_Tests LoggerSObjectMetadata @@ -154,6 +156,10 @@ LogEntryEvent__e.ExceptionSourceMetadataType__c LogEntryEvent__e.ExceptionStackTrace__c LogEntryEvent__e.ExceptionType__c + LogEntryEvent__e.ExternalServiceId__c + LogEntryEvent__e.ExternalServiceName__c + LogEntryEvent__e.ExternalServiceType__c + LogEntryEvent__e.ExternalServiceVersion__c LogEntryEvent__e.HttpRequestBodyMasked__c LogEntryEvent__e.HttpRequestBody__c LogEntryEvent__e.HttpRequestCompressed__c @@ -531,6 +537,10 @@ Log__c.ClosedDate__c Log__c.Comments__c Log__c.EndTime__c + Log__c.ExternalServiceId__c + Log__c.ExternalServiceName__c + Log__c.ExternalServiceType__c + Log__c.ExternalServiceVersion__c Log__c.HasComments__c Log__c.HasLoggedByFederationIdentifier__c Log__c.HasOrganizationLimits__c @@ -861,6 +871,7 @@ Log__c.AllBatchLogs Log__c.AllChildLogs Log__c.AllClosedLogs + Log__c.AllExternalServiceLogs Log__c.AllImpersonatedLogs Log__c.AllLogs Log__c.AllLogsWithERROREntries @@ -890,6 +901,7 @@ LoggerAdmin LoggerEndUser + LoggerIntegration LoggerLogCreator LoggerLogViewer PermissionSet diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index 6bc8a4c32..a2984c454 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -158,6 +158,10 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { AsyncContextParentJobId__c = logEntryEvent.AsyncContextParentJobId__c, AsyncContextTriggerId__c = logEntryEvent.AsyncContextTriggerId__c, AsyncContextType__c = logEntryEvent.AsyncContextType__c, + ExternalServiceId__c = logEntryEvent.ExternalServiceId__c, + ExternalServiceName__c = logEntryEvent.ExternalServiceName__c, + ExternalServiceType__c = logEntryEvent.ExternalServiceType__c, + ExternalServiceVersion__c = logEntryEvent.ExternalServiceVersion__c, ImpersonatedBy__c = logEntryEvent.ImpersonatedById__c, Locale__c = logEntryEvent.Locale__c, LoggedBy__c = logEntryEvent.LoggedById__c, diff --git a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls index 11a03a500..e2eef4f10 100644 --- a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls +++ b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls @@ -424,7 +424,13 @@ public without sharing virtual class LogManagementDataSelector { return new List(); } - return [SELECT Id, Name, Username, SmallPhotoUrl FROM User WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm ORDER BY Username LIMIT 20]; + return [ + SELECT Id, Name, Username, SmallPhotoUrl + FROM User + WHERE IsActive = TRUE AND (Name LIKE :searchTerm OR Username LIKE :searchTerm) + ORDER BY Username + LIMIT 20 + ]; } /** 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..909297b9b --- /dev/null +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls @@ -0,0 +1,644 @@ +//------------------------------------------------------------------------------------------------// +// 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) { + Integer transactionEntryNumber = 1; + for (OTelLogRecord otelLogEntry : scopeLog.logRecords) { + LogEntryEvent__e convertedLogEntryEvent = otelLogEntry.getLogEntryEvent(); + convertedLogEntryEvent.TransactionEntryNumber__c = transactionEntryNumber++; + Map supplementalFieldToValue = this.resource.convertAttributes(); + for (Schema.SObjectField field : supplementalFieldToValue.keySet()) { + convertedLogEntryEvent.put(field, supplementalFieldToValue.get(field)); + } + logEntryEvents.add(convertedLogEntryEvent); + } + } + + return logEntryEvents; + } + } + + // OTel supports an additional type float64Value + // but there's not currently a need for them in Nebula Logger's data model + 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.id' { + supplementalFieldToValue.put(LogEntryEvent__e.ExternalServiceId__c, entryAttribute.value?.stringValue); + } + when 'service.name' { + supplementalFieldToValue.put(LogEntryEvent__e.ExternalServiceName__c, entryAttribute.value?.stringValue); + } + when 'service.type' { + supplementalFieldToValue.put(LogEntryEvent__e.ExternalServiceType__c, entryAttribute.value?.stringValue); + } + when 'service.version' { + supplementalFieldToValue.put(LogEntryEvent__e.ExternalServiceVersion__c, entryAttribute.value?.stringValue); + } + } + } + + 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 name; + public Integer severityNumber; + public String severityText; + // TODO revisit mapping for spanId + public String spanId; + public String timeUnixNano; + public String traceId; + + private transient LogEntryEvent__e convertedLogEntryEvent; + + public LogEntryEvent__e getLogEntryEvent() { + if (this.convertedLogEntryEvent == null) { + System.LoggingLevel entryLoggingLevel = this.getLoggingLevel(); + Long entryEpochTimestamp = timeUnixNano == null ? null : Long.valueOf(this.timeUnixNano) / 1000000; + Datetime entryTimestamp = timeUnixNano == null ? null : Datetime.newInstance(entryEpochTimestamp); + String convertedTraceId = this.convertTraceId(); + + LogEntryEventBuilder builder = Logger.newEntry(entryLoggingLevel, this.body?.stringValue); + if (entryTimestamp != null) { + builder.setTimestamp(entryTimestamp); + } + this.convertedLogEntryEvent = builder.getLogEntryEvent(); + this.convertedLogEntryEvent.EntryScenario__c = this.name; + this.convertedLogEntryEvent.OriginType__c = 'External Service'; + this.convertedLogEntryEvent.TransactionId__c = convertedTraceId; + + // 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; + + // Since the log entries originate off-platform, the loggedBy user + // may not be the API user creating the logs, so clear the related fields + this.convertedLogEntryEvent.Locale__c = null; + this.convertedLogEntryEvent.LoggedById__c = null; + this.convertedLogEntryEvent.ProfileId__c = null; + this.convertedLogEntryEvent.ThemeDisplayed__c = null; + this.convertedLogEntryEvent.TimeZoneId__c = null; + this.convertedLogEntryEvent.TimeZoneName__c = null; + this.convertedLogEntryEvent.UserLicenseDefinitionKey__c = null; + this.convertedLogEntryEvent.UserLicenseId__c = null; + this.convertedLogEntryEvent.UserLicenseName__c = null; + this.convertedLogEntryEvent.UserRoleId__c = null; + this.convertedLogEntryEvent.UserRoleName__c = null; + this.convertedLogEntryEvent.UserType__c = null; + + // Clear irrelevant origin fields + 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.StackTrace__c = null; + + Map supplementalFieldToValue = this.convertAttributes(); + for (Schema.SObjectField field : supplementalFieldToValue.keySet()) { + this.convertedLogEntryEvent.put(field, supplementalFieldToValue.get(field)); + } + } + + return this.convertedLogEntryEvent; + } + + private Map getSeverityNumberToTextMapping() { + return new Map{ + 1 => 'TRACE', + 2 => 'TRACE2', + 3 => 'TRACE3', + 4 => 'TRACE4', + 5 => 'DEBUG', + 6 => 'DEBUG2', + 7 => 'DEBUG3', + 8 => 'DEBUG4', + 9 => 'INFO', + 10 => 'INFO2', + 11 => 'INFO3', + 12 => 'INFO4', + 13 => 'WARN', + 14 => 'WARN2', + 15 => 'WARN3', + 16 => 'WARN4', + 17 => 'ERROR', + 18 => 'ERROR2', + 19 => 'ERROR3', + 20 => 'ERROR4', + 21 => 'FATAL', + 22 => 'FATAL2', + 23 => 'FATAL3', + 24 => 'FATAL4' + }; + } + + private System.LoggingLevel getLoggingLevel() { + String severityText = this.severityText ?? this.getSeverityNumberToTextMapping().get(this.severityNumber); + // Docs: https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext + switch on severityText?.toUpperCase() { + when 'FATAL4', 'FATAL3', 'FATAL2', 'FATAL', 'ERROR4', 'ERROR3', 'ERROR2', 'ERROR' { + return System.LoggingLevel.ERROR; + } + when 'WARN4', 'WARN3', 'WARN2', 'WARN' { + return System.LoggingLevel.WARN; + } + when 'INFO4', 'INFO3', 'INFO2', 'INFO' { + return System.LoggingLevel.INFO; + } + when 'DEBUG4', 'DEBUG3', 'DEBUG2', 'DEBUG' { + return System.LoggingLevel.DEBUG; + } + when 'TRACE4', '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 'browser.address' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserAddress__c, entryAttribute.value?.stringValue); + } + when 'browser.form_factor' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserFormFactor__c, entryAttribute.value?.stringValue); + } + when 'browser.language' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserLanguage__c, entryAttribute.value?.stringValue); + } + when 'browser.screen_resolution' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserScreenResolution__c, entryAttribute.value?.stringValue); + } + when 'browser.user_agent' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserUserAgent__c, entryAttribute.value?.stringValue); + } + when 'browser.window_resolution' { + supplementalFieldToValue.put(LogEntryEvent__e.BrowserWindowResolution__c, entryAttribute.value?.stringValue); + } + 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 'http_request.body' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestBody__c, entryAttribute.value?.stringValue); + } + when 'http_request.body_masked' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestBodyMasked__c, entryAttribute.value?.boolValue); + } + when 'http_request.compressed' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestCompressed__c, entryAttribute.value?.boolValue); + } + when 'http_request.endpoint' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestEndpoint__c, entryAttribute.value?.stringValue); + } + when 'http_request.header_keys' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestHeaderKeys__c, entryAttribute.value?.stringValue); + } + when 'http_request.headers' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestHeaders__c, entryAttribute.value?.stringValue); + } + when 'http_request.method' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpRequestMethod__c, entryAttribute.value?.stringValue); + } + when 'http_response.body' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseBody__c, entryAttribute.value?.stringValue); + } + when 'http_response.body_masked' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseBodyMasked__c, entryAttribute.value?.boolValue); + } + when 'http_response.header_keys' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseHeaderKeys__c, entryAttribute.value?.stringValue); + } + when 'http_response.headers' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseHeaders__c, entryAttribute.value?.stringValue); + } + when 'http_response.status' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseStatus__c, entryAttribute.value?.stringValue); + } + when 'http_response.status_code' { + supplementalFieldToValue.put(LogEntryEvent__e.HttpResponseStatusCode__c, entryAttribute.value?.intValue); + } + when 'logged_by.federation_identifier' { + supplementalFieldToValue.put(LogEntryEvent__e.LoggedByFederationIdentifier__c, entryAttribute.value?.stringValue); + } + when 'logged_by.id' { + supplementalFieldToValue.put(LogEntryEvent__e.LoggedById__c, entryAttribute.value?.stringValue); + } + when 'logged_by.username' { + supplementalFieldToValue.put(LogEntryEvent__e.LoggedByUsername__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; + } + + private String convertTraceId() { + if (String.isBlank(this.traceId)) { + return null; + } + + String hyphenatedUuid = + this.traceId.substring(0, 8) + + '-' + + this.traceId.substring(8, 12) + + '-' + + this.traceId.substring(12, 16) + + '-' + + this.traceId.substring(16, 20) + + '-' + + this.traceId.substring(20, 32); + + return hyphenatedUuid; + } + } +} 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..800ee4289 --- /dev/null +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml b/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml index 5263621e1..8a26407c4 100644 --- a/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml +++ b/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml @@ -312,6 +312,86 @@ Facet + + + + uiBehavior + none + + Record.ExternalServiceId__c + RecordExternalServiceId_cField2 + + + {!Record.ExternalServiceId__c} + NE + + + + + + + + uiBehavior + none + + Record.ExternalServiceName__c + RecordExternalServiceName_cField + + + {!Record.ExternalServiceName__c} + NE + + + + + + + + uiBehavior + none + + Record.ExternalServiceType__c + RecordExternalServiceType_cField + + + {!Record.ExternalServiceType__c} + NE + + + + + + + + uiBehavior + none + + Record.ExternalServiceVersion__c + RecordExternalServiceVersion_cField + + + {!Record.ExternalServiceVersion__c} + NE + + + + + + + + uiBehavior + readonly + + Record.ParentLogLink__c + RecordParentLogLink__cField + + + {!Record.ParentLogTransactionId__c} + NE + + + + @@ -348,6 +428,10 @@ RecordStartTime__cField + Facet-d581561a-9af6-472b-bf5b-f191cbaa0836 + Facet + + @@ -368,10 +452,6 @@ RecordTotalLimitsCpuTimeUsed__cField - Facet-d581561a-9af6-472b-bf5b-f191cbaa0836 - Facet - - @@ -402,16 +482,6 @@ RecordTotalWARNLogEntries__cField - - - - uiBehavior - readonly - - Record.ParentLogLink__c - RecordParentLogLink__cField - - Facet-af147cb3-e6b1-4ef7-a00b-d4746d5a1c90 Facet 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..f5bb6865c 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 Component - #A845DC + #8AC926 false + + External Service + #FFCA3A + false + + Flow - #FFCC33 + #1982C4 false OmniStudio - #FFCC33 + #6A4C93 false diff --git a/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml new file mode 100644 index 000000000..1bcce08c0 --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml @@ -0,0 +1,14 @@ + + + ExternalServiceId__c + Active + None + true + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceName__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceName__c.field-meta.xml new file mode 100644 index 000000000..4bc9a645b --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceName__c.field-meta.xml @@ -0,0 +1,14 @@ + + + ExternalServiceName__c + Active + None + true + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceType__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceType__c.field-meta.xml new file mode 100644 index 000000000..86ae834ac --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceType__c.field-meta.xml @@ -0,0 +1,14 @@ + + + ExternalServiceType__c + Active + None + true + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceVersion__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceVersion__c.field-meta.xml new file mode 100644 index 000000000..670d157a2 --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceVersion__c.field-meta.xml @@ -0,0 +1,14 @@ + + + ExternalServiceVersion__c + Active + None + true + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/core/main/log-management/objects/Log__c/listViews/AllExternalServiceLogs.listView-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/listViews/AllExternalServiceLogs.listView-meta.xml new file mode 100644 index 000000000..fa6c42fc8 --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/listViews/AllExternalServiceLogs.listView-meta.xml @@ -0,0 +1,25 @@ + + + AllExternalServiceLogs + NAME + ExternalServiceType__c + ExternalServiceId__c + ExternalServiceName__c + LoggedByUsernameLink__c + ProfileLink__c + TransactionId__c + TotalLogEntries__c + TotalERRORLogEntries__c + TotalWARNLogEntries__c + OWNER.ALIAS + Priority__c + Status__c + TransactionScenarioLink__c + StartTime__c + Everything + + ExternalServiceName__c + notEqual + + + 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 956f753ed..002b86654 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 @@ -72,6 +72,10 @@ LoggerParameter true + + LoggerRestResource + true + LoggerSObjectMetadata true @@ -1246,6 +1250,26 @@ Log__c.EndTime__c true + + false + Log__c.ExternalServiceId__c + true + + + false + Log__c.ExternalServiceName__c + true + + + false + Log__c.ExternalServiceType__c + true + + + false + Log__c.ExternalServiceVersion__c + true + false Log__c.HasComments__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 8e057f3f9..31a24c18b 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 @@ -777,6 +777,26 @@ Log__c.EndTime__c true + + false + Log__c.ExternalServiceId__c + true + + + false + Log__c.ExternalServiceName__c + true + + + false + Log__c.ExternalServiceType__c + true + + + false + Log__c.ExternalServiceVersion__c + true + false Log__c.HasLoggedByFederationIdentifier__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml new file mode 100644 index 000000000..2a0c7d8e7 --- /dev/null +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.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/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml index 438fb83dd..747cf096e 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 @@ -1166,6 +1166,26 @@ Log__c.EndTime__c true + + false + Log__c.ExternalServiceId__c + true + + + false + Log__c.ExternalServiceName__c + true + + + false + Log__c.ExternalServiceType__c + true + + + false + Log__c.ExternalServiceVersion__c + true + false Log__c.HasComments__c diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 769c21b0f..5bfce9495 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.15.3'; + private static final String CURRENT_VERSION_NUMBER = 'v4.15.4'; 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/loggerService.js b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js index 8c775666b..4c7bd212a 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js @@ -10,7 +10,7 @@ import LoggerServiceTaskQueue from './loggerServiceTaskQueue'; import getSettings from '@salesforce/apex/ComponentLogger.getSettings'; import saveComponentLogEntries from '@salesforce/apex/ComponentLogger.saveComponentLogEntries'; -const CURRENT_VERSION_NUMBER = 'v4.15.3'; +const CURRENT_VERSION_NUMBER = 'v4.15.4'; const CONSOLE_OUTPUT_CONFIG = { messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `, diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml new file mode 100644 index 000000000..e01414cbf --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml @@ -0,0 +1,16 @@ + + + ExternalServiceId__c + Active + None + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceName__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceName__c.field-meta.xml new file mode 100644 index 000000000..a342b5cc5 --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceName__c.field-meta.xml @@ -0,0 +1,16 @@ + + + ExternalServiceName__c + Active + None + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceType__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceType__c.field-meta.xml new file mode 100644 index 000000000..cbeb00706 --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceType__c.field-meta.xml @@ -0,0 +1,16 @@ + + + ExternalServiceType__c + Active + None + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceVersion__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceVersion__c.field-meta.xml new file mode 100644 index 000000000..8fae64dec --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceVersion__c.field-meta.xml @@ -0,0 +1,16 @@ + + + ExternalServiceVersion__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 7c87d3f0c..2424a38e9 100644 --- a/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml +++ b/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml @@ -25,6 +25,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 747d68ce6..4fce9f94d 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -1303,6 +1303,10 @@ private class LogEntryEventHandler_Tests { AsyncContextParentJobId__c, AsyncContextTriggerId__c, AsyncContextType__c, + ExternalServiceId__c, + ExternalServiceName__c, + ExternalServiceType__c, + ExternalServiceVersion__c, Id, Locale__c, LoggedBy__c, @@ -1496,6 +1500,10 @@ private class LogEntryEventHandler_Tests { System.Assert.areEqual(logEntryEvent.AsyncContextParentJobId__c, log.AsyncContextParentJobId__c, 'log.AsyncContextParentJobId__c was not properly set'); System.Assert.areEqual(logEntryEvent.AsyncContextTriggerId__c, log.AsyncContextTriggerId__c, 'log.AsyncContextTriggerId__c was not properly set'); System.Assert.areEqual(logEntryEvent.AsyncContextType__c, log.AsyncContextType__c, 'log.AsyncContextTriggerId__c was not properly set'); + System.Assert.areEqual(logEntryEvent.ExternalServiceId__c, log.ExternalServiceId__c, 'log.ExternalServiceId__c was not properly set'); + System.Assert.areEqual(logEntryEvent.ExternalServiceName__c, log.ExternalServiceName__c, 'log.ExternalServiceName__c was not properly set'); + System.Assert.areEqual(logEntryEvent.ExternalServiceType__c, log.ExternalServiceType__c, 'log.ExternalServiceType__c was not properly set'); + System.Assert.areEqual(logEntryEvent.ExternalServiceVersion__c, log.ExternalServiceVersion__c, 'log.ExternalServiceVersion__c was not properly set'); System.Assert.areEqual(logEntryEvent.Locale__c, log.Locale__c, 'log.Locale__c was not properly set'); System.Assert.areEqual( logEntryEvent.LoggedByFederationIdentifier__c, diff --git a/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls index 3d9804efe..8f17a9727 100644 --- a/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls @@ -580,7 +580,7 @@ private class LogManagementDataSelector_Tests { List expectedResults = [ SELECT Id, Name, Username, SmallPhotoUrl FROM User - WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm + WHERE IsActive = TRUE AND (Name LIKE :searchTerm OR Username LIKE :searchTerm) ]; List returnedResults = LogManagementDataSelector.getInstance().getUsersByNameSearch(searchTerm); 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..25b4afabf --- /dev/null +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls @@ -0,0 +1,374 @@ +//------------------------------------------------------------------------------------------------// +// 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() { + // This test method is... incredibly long. The work is "simple", it's "just" creating a bunch of OTel attributes + // and validating that they map to the correct LogEntryEvent__e fields. But the lines of code is a lot... + // TODO revisit to see if there's a way to shorten this up/make it more readable + 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.name = 'Some span name, which maps to Nebula Logger\'s scenario name'; + otelLogEntry.severityText = 'Info'; + // otelLogEntry.spanId = 'TODO'; + otelLogEntry.timeUnixNano = (System.now().getTime() * 1000000).toString(); + otelLogEntry.traceId = System.UUID.randomUUID().toString().replace('-', '').toLowerCase(); + LoggerRestResource.OTelAttribute browserAddressAttribute = new LoggerRestResource.OTelAttribute('browser.address', 'some browser address'); + otelLogEntry.attributes.add(browserAddressAttribute); + LoggerRestResource.OTelAttribute browserFormFactorAttribute = new LoggerRestResource.OTelAttribute('browser.form_factor', 'some browser form factor'); + otelLogEntry.attributes.add(browserFormFactorAttribute); + LoggerRestResource.OTelAttribute browserLanguageAttribute = new LoggerRestResource.OTelAttribute('browser.language', 'some browser language'); + otelLogEntry.attributes.add(browserLanguageAttribute); + LoggerRestResource.OTelAttribute browserScreenResolutionAttribute = new LoggerRestResource.OTelAttribute( + 'browser.screen_resolution', + 'some browser screen resolution' + ); + otelLogEntry.attributes.add(browserScreenResolutionAttribute); + LoggerRestResource.OTelAttribute browserUserAgentAttribute = new LoggerRestResource.OTelAttribute('browser.user_agent', 'some browser user agent'); + otelLogEntry.attributes.add(browserUserAgentAttribute); + LoggerRestResource.OTelAttribute browserWindowResolutionAttribute = new LoggerRestResource.OTelAttribute( + 'browser.window_resolution', + 'some browser window resolution' + ); + otelLogEntry.attributes.add(browserWindowResolutionAttribute); + 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 httpRequestBodyAttribute = new LoggerRestResource.OTelAttribute('http_request.body', 'some value for http_request.body'); + otelLogEntry.attributes.add(httpRequestBodyAttribute); + LoggerRestResource.OTelAttribute httpRequestBodyMaskedAttribute = new LoggerRestResource.OTelAttribute('http_request.body_masked', true); + otelLogEntry.attributes.add(httpRequestBodyMaskedAttribute); + LoggerRestResource.OTelAttribute httpRequestCompressedAttribute = new LoggerRestResource.OTelAttribute('http_request.compressed', false); + otelLogEntry.attributes.add(httpRequestCompressedAttribute); + LoggerRestResource.OTelAttribute httpRequestEndpointAttribute = new LoggerRestResource.OTelAttribute( + 'http_request.endpoint', + 'some value for http_request.endpoint' + ); + otelLogEntry.attributes.add(httpRequestEndpointAttribute); + LoggerRestResource.OTelAttribute httpRequestHeaderKeysAttribute = new LoggerRestResource.OTelAttribute( + 'http_request.header_keys', + 'some value for http_request.header_keys' + ); + otelLogEntry.attributes.add(httpRequestHeaderKeysAttribute); + LoggerRestResource.OTelAttribute httpRequestHeadersAttribute = new LoggerRestResource.OTelAttribute( + 'http_request.headers', + 'some value for http_request.headers' + ); + otelLogEntry.attributes.add(httpRequestHeadersAttribute); + LoggerRestResource.OTelAttribute httpRequestMethodAttribute = new LoggerRestResource.OTelAttribute( + 'http_request.method', + 'some value for http_request.method' + ); + otelLogEntry.attributes.add(httpRequestMethodAttribute); + LoggerRestResource.OTelAttribute httpResponseBodyAttribute = new LoggerRestResource.OTelAttribute( + 'http_response.body', + 'some value for http_response.body' + ); + otelLogEntry.attributes.add(httpResponseBodyAttribute); + LoggerRestResource.OTelAttribute httpResponseBodyMaskedAttribute = new LoggerRestResource.OTelAttribute('http_response.body_masked', true); + otelLogEntry.attributes.add(httpResponseBodyMaskedAttribute); + LoggerRestResource.OTelAttribute httpResponseHeaderKeysAttribute = new LoggerRestResource.OTelAttribute( + 'http_response.header_keys', + 'some value for http_response.header_keys' + ); + otelLogEntry.attributes.add(httpResponseHeaderKeysAttribute); + LoggerRestResource.OTelAttribute httpResponseHeadersAttribute = new LoggerRestResource.OTelAttribute( + 'http_response.headers', + 'some value for http_response.headers' + ); + otelLogEntry.attributes.add(httpResponseHeadersAttribute); + LoggerRestResource.OTelAttribute httpResponseStatusAttribute = new LoggerRestResource.OTelAttribute( + 'http_response.status', + 'some value for http_response.status' + ); + otelLogEntry.attributes.add(httpResponseStatusAttribute); + LoggerRestResource.OTelAttribute httpResponseStatusCodeAttribute = new LoggerRestResource.OTelAttribute('http_response.status_code', 123); + otelLogEntry.attributes.add(httpResponseStatusCodeAttribute); + LoggerRestResource.OTelAttribute loggedByFederationIdentifierAttribute = new LoggerRestResource.OTelAttribute( + 'logged_by.federation_identifier', + 'Some.Federation.Identifier@saml.system.sso.com.org' + ); + otelLogEntry.attributes.add(loggedByFederationIdentifierAttribute); + LoggerRestResource.OTelAttribute loggedByIdAttribute = new LoggerRestResource.OTelAttribute( + 'logged_by.id', + LoggerMockDataCreator.createId(Schema.User.SObjectType) + ); + otelLogEntry.attributes.add(loggedByIdAttribute); + LoggerRestResource.OTelAttribute loggedByUsernameAttribute = new LoggerRestResource.OTelAttribute('logged_by.username', 'Some.Username@some.company.com'); + otelLogEntry.attributes.add(loggedByUsernameAttribute); + 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 resourceServiceIdAttribute = new LoggerRestResource.OTelAttribute('service.id', 'some-unique-id-value'); + resourceLog.resource.attributes.add(resourceServiceIdAttribute); + LoggerRestResource.OTelAttribute resourceServiceNameAttribute = new LoggerRestResource.OTelAttribute( + 'service.name', + 'some-external-system-or-microservice' + ); + resourceLog.resource.attributes.add(resourceServiceNameAttribute); + LoggerRestResource.OTelAttribute resourceServiceTypeAttribute = new LoggerRestResource.OTelAttribute( + 'service.type', + 'some-type-of-external-system-or-microservice' + ); + resourceLog.resource.attributes.add(resourceServiceTypeAttribute); + LoggerRestResource.OTelAttribute resourceServiceVersionAttribute = new LoggerRestResource.OTelAttribute('service.version', 'some-version-number'); + resourceLog.resource.attributes.add(resourceServiceVersionAttribute); + 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); + // External Service Fields + System.Assert.areEqual(resourceServiceIdAttribute.value.stringValue, publishedLogEntryEvent.ExternalServiceId__c); + System.Assert.areEqual(resourceServiceNameAttribute.value.stringValue, publishedLogEntryEvent.ExternalServiceName__c); + System.Assert.areEqual(resourceServiceTypeAttribute.value.stringValue, publishedLogEntryEvent.ExternalServiceType__c); + System.Assert.areEqual(resourceServiceVersionAttribute.value.stringValue, publishedLogEntryEvent.ExternalServiceVersion__c); + // Origin Fields + 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('External Service', publishedLogEntryEvent.OriginType__c); + System.Assert.areEqual(otelLogEntry.severityText.toUpperCase(), publishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(otelLogEntry.body.stringValue, publishedLogEntryEvent.Message__c); + // Limits Fields + System.Assert.isNull(publishedLogEntryEvent.LimitsAggregateQueriesMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsAggregateQueriesUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsAsyncCallsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsAsyncCallsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsCalloutsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsCalloutsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsCpuTimeMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsCpuTimeUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsDmlRowsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsDmlRowsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsDmlStatementsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsDmlStatementsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsEmailInvocationsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsEmailInvocationsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsFutureCallsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsFutureCallsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsHeapSizeMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsHeapSizeUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsMobilePushApexCallsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsMobilePushApexCallsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsPublishImmediateDmlStatementsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsPublishImmediateDmlStatementsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsQueueableJobsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsQueueableJobsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueriesMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueriesUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueryLocatorRowsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueryLocatorRowsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueryRowsMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoqlQueryRowsUsed__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoslSearchesMax__c); + System.Assert.isNull(publishedLogEntryEvent.LimitsSoslSearchesUsed__c); + // User Fields (that should not be auto-populated) + System.Assert.isNull(publishedLogEntryEvent.Locale__c); + System.Assert.isNull(publishedLogEntryEvent.ProfileId__c); + System.Assert.isNull(publishedLogEntryEvent.ThemeDisplayed__c); + System.Assert.isNull(publishedLogEntryEvent.TimeZoneId__c); + System.Assert.isNull(publishedLogEntryEvent.TimeZoneName__c); + System.Assert.isNull(publishedLogEntryEvent.UserLicenseDefinitionKey__c); + System.Assert.isNull(publishedLogEntryEvent.UserLicenseId__c); + System.Assert.isNull(publishedLogEntryEvent.UserLicenseName__c); + System.Assert.isNull(publishedLogEntryEvent.UserRoleId__c); + System.Assert.isNull(publishedLogEntryEvent.UserRoleName__c); + System.Assert.isNull(publishedLogEntryEvent.UserType__c); + // Browser Fields + System.Assert.areEqual(browserAddressAttribute.value.stringValue, publishedLogEntryEvent.BrowserAddress__c); + System.Assert.areEqual(browserFormFactorAttribute.value.stringValue, publishedLogEntryEvent.BrowserFormFactor__c); + System.Assert.areEqual(browserLanguageAttribute.value.stringValue, publishedLogEntryEvent.BrowserLanguage__c); + System.Assert.areEqual(browserScreenResolutionAttribute.value.stringValue, publishedLogEntryEvent.BrowserScreenResolution__c); + System.Assert.areEqual(browserUserAgentAttribute.value.stringValue, publishedLogEntryEvent.BrowserUserAgent__c); + System.Assert.areEqual(browserWindowResolutionAttribute.value.stringValue, publishedLogEntryEvent.BrowserWindowResolution__c); + // Exception Fields + 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); + // HTTP Request Fields + System.Assert.areEqual(httpRequestBodyMaskedAttribute.value.boolValue, publishedLogEntryEvent.HttpRequestBodyMasked__c); + System.Assert.areEqual(httpRequestCompressedAttribute.value.boolValue, publishedLogEntryEvent.HttpRequestCompressed__c); + System.Assert.areEqual(httpRequestEndpointAttribute.value.stringValue, publishedLogEntryEvent.HttpRequestEndpoint__c); + System.Assert.areEqual(httpRequestHeaderKeysAttribute.value.stringValue, publishedLogEntryEvent.HttpRequestHeaderKeys__c); + System.Assert.areEqual(httpRequestHeadersAttribute.value.stringValue, publishedLogEntryEvent.HttpRequestHeaders__c); + System.Assert.areEqual(httpRequestMethodAttribute.value.stringValue, publishedLogEntryEvent.HttpRequestMethod__c); + System.Assert.areEqual(httpResponseBodyAttribute.value.stringValue, publishedLogEntryEvent.HttpResponseBody__c); + // HTTP Response Fields + System.Assert.areEqual(httpResponseBodyMaskedAttribute.value.boolValue, publishedLogEntryEvent.HttpResponseBodyMasked__c); + System.Assert.areEqual(httpResponseHeaderKeysAttribute.value.stringValue, publishedLogEntryEvent.HttpResponseHeaderKeys__c); + System.Assert.areEqual(httpResponseHeadersAttribute.value.stringValue, publishedLogEntryEvent.HttpResponseHeaders__c); + System.Assert.areEqual(httpResponseStatusAttribute.value.stringValue, publishedLogEntryEvent.HttpResponseStatus__c); + System.Assert.areEqual(httpResponseStatusCodeAttribute.value.intValue, publishedLogEntryEvent.HttpResponseStatusCode__c); + // Logged By Fields + System.Assert.areEqual(loggedByFederationIdentifierAttribute.value.stringValue, publishedLogEntryEvent.LoggedByFederationIdentifier__c); + System.Assert.areEqual(loggedByIdAttribute.value.stringValue, publishedLogEntryEvent.LoggedById__c); + System.Assert.areEqual(loggedByUsernameAttribute.value.stringValue, publishedLogEntryEvent.LoggedByUsername__c); + // Other Fields + 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); + System.Assert.areEqual(otelLogEntry.name, publishedLogEntryEvent.EntryScenario__c); + String hyphenatedUuid = + otelLogEntry.traceId.substring(0, 8) + + '-' + + otelLogEntry.traceId.substring(8, 12) + + '-' + + otelLogEntry.traceId.substring(12, 16) + + '-' + + otelLogEntry.traceId.substring(16, 20) + + '-' + + otelLogEntry.traceId.substring(20, 32); + String expectedTransactionId = System.UUID.fromString(hyphenatedUuid).toString(); + System.Assert.areEqual(expectedTransactionId, publishedLogEntryEvent.TransactionId__c); + System.Assert.areEqual(1, publishedLogEntryEvent.TransactionEntryNumber__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..800ee4289 --- /dev/null +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.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 1fd6e8f43..adb421390 100644 --- a/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/ComponentLogger_Tests.cls @@ -74,7 +74,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 f4cb2ba85..3ff0f53ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.15.3", + "version": "4.15.4", "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", diff --git a/sfdx-project.json b/sfdx-project.json index 9c19742cd..24ffc41f9 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -9,10 +9,9 @@ "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.15.3.NEXT", - "versionName": "Improved Testability of Queries", - "versionDescription": "Enhanced the internals of the existing selector classes LoggerEngineDataSelector and LogManagementDataSelector + introducted a new class LoggerConfigurationSelector to provide improve testability of queries", - "postInstallUrl": "https://github.com/jongpie/NebulaLogger/wiki", + "versionNumber": "4.15.4.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" @@ -208,6 +207,7 @@ "Nebula Logger - Core@4.15.1-system.orglimits-optimisations": "04t5Y0000015ohhQAA", "Nebula Logger - Core@4.15.2-added-support-for-logging-emptyrecylebinresult": "04t5Y0000015oifQAA", "Nebula Logger - Core@4.15.3-improved-testability-of-queries": "04t5Y0000015ok2QAA", + "Nebula Logger - Core@4.15.4-opentelemetry-(otel)-rest-resource": "04t5Y0000015okMQAQ", "Nebula Logger - Core Plugin - Async Failure Additions": "0Ho5Y000000blO4SAI", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.0": "04t5Y0000015lhiQAA", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.1": "04t5Y0000015lhsQAA",