From e828e282826b5fc4abeb4209efb64dbf9be8a21d Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Wed, 18 Sep 2024 10:03:26 -0400 Subject: [PATCH 1/8] 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 +- .../logger-engine/lwc/logger/loggerService.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 | 2 +- sfdx-project.json | 7 +- 23 files changed, 988 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 7f8ff2f76..95416e0d3 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, 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) @@ -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/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index 6bc8a4c32..f24843806 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -339,6 +339,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 956f753ed..d5ec788b3 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 @@ -1001,6 +1005,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 8e057f3f9..5cbf8a757 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 @@ -627,6 +627,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 438fb83dd..b28ebd0a2 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 @@ -921,6 +921,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 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/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 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..5a96e6370 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -1440,6 +1440,7 @@ private class LogEntryEventHandler_Tests { OriginSourceApiName__c, OriginSourceId__c, OriginSourceMetadataType__c, + OriginSystemName__c, OriginType__c, RecordCollectionSize__c, RecordCollectionType__c, @@ -1761,6 +1762,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 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..d54a3639f 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" From 6512bb254509eff3236fdf6a4962ae505dd25fbe Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Wed, 18 Sep 2024 22:29:39 -0400 Subject: [PATCH 2/8] [WIP] Renamed fields OriginSystemName__c to OriginServiceName__c --- .../log-management/classes/LogEntryEventHandler.cls | 2 +- .../classes/LogManagementDataSelector.cls | 8 +++++++- .../main/log-management/classes/LoggerRestResource.cls | 2 +- ...ld-meta.xml => OriginServiceName__c.field-meta.xml} | 4 ++-- .../listViews/AllApiLogEntries.listView-meta.xml | 2 +- .../permissionsets/LoggerAdmin.permissionset-meta.xml | 10 +++++----- .../LoggerEndUser.permissionset-meta.xml | 6 +++--- .../LoggerLogViewer.permissionset-meta.xml | 10 +++++----- ...ld-meta.xml => OriginServiceName__c.field-meta.xml} | 2 +- .../classes/LogEntryEventHandler_Tests.cls | 4 ++-- .../classes/LogManagementDataSelector_Tests.cls | 2 +- .../classes/LoggerRestResource_Tests.cls | 2 +- 12 files changed, 30 insertions(+), 24 deletions(-) rename nebula-logger/core/main/log-management/objects/LogEntry__c/fields/{OriginSystemName__c.field-meta.xml => OriginServiceName__c.field-meta.xml} (84%) rename nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/{OriginSystemName__c.field-meta.xml => OriginServiceName__c.field-meta.xml} (92%) diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index f24843806..aac5aa379 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -335,11 +335,11 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { MessageTruncated__c = logEntryEvent.MessageTruncated__c, Name = null, // Salesforce will auto-set the record ID as the name when null OriginLocation__c = logEntryEvent.OriginLocation__c, + OriginServiceName__c = logEntryEvent.OriginServiceName__c, OriginSourceActionName__c = logEntryEvent.OriginSourceActionName__c, 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/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 index 1ef85a132..6a0e8768e 100644 --- a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls @@ -343,7 +343,7 @@ global with sharing class LoggerRestResource { for (OTelAttribute entryAttribute : this.attributes) { switch on entryAttribute.key { when 'service.name' { - supplementalFieldToValue.put(LogEntryEvent__e.OriginSystemName__c, entryAttribute.value?.stringValue); + supplementalFieldToValue.put(LogEntryEvent__e.OriginServiceName__c, entryAttribute.value?.stringValue); } // TODO // when 'service.version' { 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/OriginServiceName__c.field-meta.xml similarity index 84% rename from nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSystemName__c.field-meta.xml rename to nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginServiceName__c.field-meta.xml index 895fe3ea6..993b8562b 100644 --- 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/OriginServiceName__c.field-meta.xml @@ -1,10 +1,10 @@ - OriginSystemName__c + OriginServiceName__c Active None true - + 255 false Confidential 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 index beef641cb..5e21d4a88 100644 --- 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 @@ -6,7 +6,7 @@ LoggingLevel__c LoggedByUsernameLink__c Message__c - OriginSystemName__c + OriginServiceName__c Timestamp__c Everything 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 d5ec788b3..070811ea9 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 @@ -935,6 +935,11 @@ LogEntry__c.OriginLocation__c true + + false + LogEntry__c.OriginServiceName__c + true + false LogEntry__c.OriginSourceActionName__c @@ -1005,11 +1010,6 @@ 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 5cbf8a757..e71684970 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 @@ -619,17 +619,17 @@ false - LogEntry__c.OriginSourceApiName__c + LogEntry__c.OriginServiceName__c true false - LogEntry__c.OriginSourceMetadataType__c + LogEntry__c.OriginSourceApiName__c true false - LogEntry__c.OriginSystemName__c + LogEntry__c.OriginSourceMetadataType__c true 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 b28ebd0a2..200ec001a 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 @@ -851,6 +851,11 @@ LogEntry__c.OriginLocation__c true + + false + LogEntry__c.OriginServiceName__c + true + false LogEntry__c.OriginSourceActionName__c @@ -921,11 +926,6 @@ LogEntry__c.OriginSourceSnippet__c true - - false - LogEntry__c.OriginSystemName__c - true - false LogEntry__c.OriginType__c 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/OriginServiceName__c.field-meta.xml similarity index 92% rename from nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginSystemName__c.field-meta.xml rename to nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginServiceName__c.field-meta.xml index 17f4fe7a6..653db4a0e 100644 --- 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/OriginServiceName__c.field-meta.xml @@ -1,6 +1,6 @@ - OriginSystemName__c + OriginServiceName__c Active None false 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 5a96e6370..2516ed170 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -1436,11 +1436,11 @@ private class LogEntryEventHandler_Tests { Id, Name, OriginLocation__c, + OriginServiceName__c, OriginSourceActionName__c, OriginSourceApiName__c, OriginSourceId__c, OriginSourceMetadataType__c, - OriginSystemName__c, OriginType__c, RecordCollectionSize__c, RecordCollectionType__c, @@ -1762,7 +1762,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.OriginServiceName__c, logEntry.OriginServiceName__c, 'logEntry.OriginServiceName__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/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 index d1aee01bf..c31ec26da 100644 --- a/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls @@ -169,11 +169,11 @@ private class LoggerRestResource_Tests { 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.areEqual(resourceServiceNameAttribute.value.stringValue, publishedLogEntryEvent.OriginServiceName__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); From 036ae0a3e26fd5f863707c049996049e893046bb Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Thu, 19 Sep 2024 22:46:26 -0400 Subject: [PATCH 3/8] [WIP] Improved OTel severity mappings, existing attribute mappings, mapped traceId to transactionId, and added more attributes for browser fields, HttpRequest fields, and HttpResponse fields --- .../classes/LoggerRestResource.cls | 213 +++++++++++++++--- .../classes/LoggerRestResource_Tests.cls | 175 +++++++++++++- 2 files changed, 353 insertions(+), 35 deletions(-) diff --git a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls index 6a0e8768e..45a4eda15 100644 --- a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls @@ -216,6 +216,7 @@ global with sharing class LoggerRestResource { return postResponse; } } + private void saveLog(OTelLogsPayload logsPayload) { LoggerDataStore.getEventBus().publishRecords(logsPayload.getConvertedLogEntryEvents()); } @@ -269,8 +270,10 @@ global with sharing class LoggerRestResource { 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)); @@ -283,27 +286,26 @@ global with sharing class LoggerRestResource { } } - // OTel supports additional types boolValue, float64Value, and intValue + // OTel supports an additional type float64Value // 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, 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, Integer value) { + this.key = key; + this.value = new OTelAttributeValue(value); + } public OTelAttribute(String key, String value) { this.key = key; @@ -312,22 +314,22 @@ global with sharing class LoggerRestResource { } public class OTelAttributeValue { - // public final Boolean boolValue; + public final Boolean boolValue; // public final Decimal float64Value; - // public final Integer intValue; + public final Integer intValue; public final String stringValue; - // public OTelAttributeValue(Boolean value) { - // this.boolValue = value; - // } + public OTelAttributeValue(Boolean value) { + this.boolValue = value; + } // public OTelAttributeValue(Decimal value) { // this.float64Value = value; // } - // public OTelAttributeValue(Integer value) { - // this.intValue = value; - // } + public OTelAttributeValue(Integer value) { + this.intValue = value; + } public OTelAttributeValue(String value) { this.stringValue = value; @@ -368,21 +370,32 @@ global with sharing class LoggerRestResource { public class OTelLogRecord { public List attributes = new List(); public OTelAttributeValue body; + public String name; + public Integer severityNumber; public String severityText; - public String timeUnixNano = (System.now().getTime() * 1000000).toString(); - // TODO revisit mappings for spanId and traceId - // public String spanId; - // public String traceId; + // 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 = Long.valueOf(this.timeUnixNano) / 1000000; - Datetime entryTimestamp = Datetime.newInstance(entryEpochTimestamp); + 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 = 'API'; + this.convertedLogEntryEvent.TransactionId__c = convertedTraceId; - 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; @@ -416,12 +429,28 @@ global with sharing class LoggerRestResource { 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.OriginType__c = 'API'; this.convertedLogEntryEvent.StackTrace__c = null; Map supplementalFieldToValue = this.convertAttributes(); @@ -433,27 +462,58 @@ global with sharing class LoggerRestResource { 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() { - switch on this.severityText?.toLowerCase() { - when 'error' { + 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 'warn' { + when 'WARN4', 'WARN3', 'WARN2', 'WARN' { return System.LoggingLevel.WARN; } - when 'info' { + when 'INFO4', 'INFO3', 'INFO2', 'INFO' { return System.LoggingLevel.INFO; } - when 'debug' { + when 'DEBUG4', 'DEBUG3', 'DEBUG2', 'DEBUG' { return System.LoggingLevel.DEBUG; } - when 'trace3' { + when 'TRACE4', 'TRACE3' { return System.LoggingLevel.FINE; } - when 'trace2' { + when 'TRACE2' { return System.LoggingLevel.FINER; } - when 'trace' { + when 'TRACE' { return System.LoggingLevel.FINEST; } when else { @@ -469,6 +529,24 @@ global with sharing class LoggerRestResource { 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); } @@ -478,6 +556,54 @@ global with sharing class LoggerRestResource { 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); } @@ -489,5 +615,24 @@ global with sharing class LoggerRestResource { 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/tests/log-management/classes/LoggerRestResource_Tests.cls b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls index c31ec26da..3eddef234 100644 --- a/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls @@ -118,19 +118,105 @@ private class LoggerRestResource_Tests { @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.timeUnixNano = '1581452773000000789'; + // 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'); @@ -168,6 +254,7 @@ private class LoggerRestResource_Tests { 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); + // Origin Fields System.Assert.isNull(publishedLogEntryEvent.OriginLocation__c); System.Assert.areEqual(resourceServiceNameAttribute.value.stringValue, publishedLogEntryEvent.OriginServiceName__c); System.Assert.isNull(publishedLogEntryEvent.OriginSourceActionName__c); @@ -177,12 +264,98 @@ private class LoggerRestResource_Tests { 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); + // 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); } } From 4a2d18b8e6eabd3bbf961903f766f80bdd4edfbd Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 29 Dec 2024 17:41:38 -0500 Subject: [PATCH 4/8] [WIP] Renamed new permission set LoggerRestIntegration to LoggerIntegration --- ...ionset-meta.xml => LoggerIntegration.permissionset-meta.xml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename nebula-logger/core/main/log-management/permissionsets/{LoggerRestIntegration.permissionset-meta.xml => LoggerIntegration.permissionset-meta.xml} (87%) diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml similarity index 87% rename from nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml rename to nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml index 3c5876720..e9abcf950 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerRestIntegration.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml @@ -6,5 +6,5 @@ Provides access to integrate with Nebula Logger via REST API calls false - + From db75832af970e183da82b83f1c6b080bac3a0db0 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 29 Dec 2024 17:46:18 -0500 Subject: [PATCH 5/8] [WIP] Cleanup & formatting --- .../permissionsets/LoggerAdmin.permissionset-meta.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 070811ea9..7aaffdcf9 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 @@ -65,15 +65,15 @@ true - LoggerRestResource + LoggerHomeHeaderController true - LoggerHomeHeaderController + LoggerParameter true - LoggerParameter + LoggerRestResource true From f0c63dfea8df5ccd7c79e7d7859d67a7e4f88dd4 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 29 Dec 2024 23:10:34 -0500 Subject: [PATCH 6/8] [WIP] Moved service name field to Log__c, added additional 'external service' fields --- .../classes/LogEntryEventHandler.cls | 5 +- .../classes/LoggerRestResource.cls | 16 ++- .../classes/LoggerRestResource.cls-meta.xml | 2 +- .../LogRecordPage.flexipage-meta.xml | 98 ++++++++++++++++--- .../fields/OriginType__c.field-meta.xml | 12 +-- .../AllApiLogEntries.listView-meta.xml | 18 ---- .../ExternalServiceId__c.field-meta.xml} | 4 +- .../ExternalServiceName__c.field-meta.xml | 14 +++ .../ExternalServiceType__c.field-meta.xml | 14 +++ .../ExternalServiceVersion__c.field-meta.xml | 14 +++ .../AllExternalServiceLogs.listView-meta.xml | 25 +++++ .../LoggerAdmin.permissionset-meta.xml | 25 ++++- .../LoggerEndUser.permissionset-meta.xml | 25 ++++- .../LoggerIntegration.permissionset-meta.xml | 2 +- .../LoggerLogViewer.permissionset-meta.xml | 25 ++++- ...ml => ExternalServiceId__c.field-meta.xml} | 4 +- .../ExternalServiceName__c.field-meta.xml | 16 +++ .../ExternalServiceType__c.field-meta.xml | 16 +++ .../ExternalServiceVersion__c.field-meta.xml | 16 +++ .../classes/LogEntryEventHandler_Tests.cls | 10 +- .../classes/LoggerRestResource_Tests.cls | 17 +++- .../LoggerRestResource_Tests.cls-meta.xml | 2 +- 22 files changed, 310 insertions(+), 70 deletions(-) delete mode 100644 nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml rename nebula-logger/core/main/log-management/objects/{LogEntry__c/fields/OriginServiceName__c.field-meta.xml => Log__c/fields/ExternalServiceId__c.field-meta.xml} (84%) create mode 100644 nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceName__c.field-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceType__c.field-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceVersion__c.field-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/Log__c/listViews/AllExternalServiceLogs.listView-meta.xml rename nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/{OriginServiceName__c.field-meta.xml => ExternalServiceId__c.field-meta.xml} (86%) create mode 100644 nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceName__c.field-meta.xml create mode 100644 nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceType__c.field-meta.xml create mode 100644 nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceVersion__c.field-meta.xml diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index aac5aa379..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, @@ -335,7 +339,6 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { MessageTruncated__c = logEntryEvent.MessageTruncated__c, Name = null, // Salesforce will auto-set the record ID as the name when null OriginLocation__c = logEntryEvent.OriginLocation__c, - OriginServiceName__c = logEntryEvent.OriginServiceName__c, OriginSourceActionName__c = logEntryEvent.OriginSourceActionName__c, OriginSourceApiName__c = logEntryEvent.OriginSourceApiName__c, OriginSourceId__c = logEntryEvent.OriginSourceId__c, diff --git a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls index 45a4eda15..909297b9b 100644 --- a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls @@ -344,12 +344,18 @@ global with sharing class LoggerRestResource { 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.OriginServiceName__c, entryAttribute.value?.stringValue); + 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); } - // TODO - // when 'service.version' { - // } } } @@ -393,7 +399,7 @@ global with sharing class LoggerRestResource { } this.convertedLogEntryEvent = builder.getLogEntryEvent(); this.convertedLogEntryEvent.EntryScenario__c = this.name; - this.convertedLogEntryEvent.OriginType__c = 'API'; + 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 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 index c01f6433a..800ee4289 100644 --- a/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml +++ b/nebula-logger/core/main/log-management/classes/LoggerRestResource.cls-meta.xml @@ -1,5 +1,5 @@ - 61.0 + 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 0cf6fcf4f..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 @@ -17,18 +17,18 @@ false - - API - #FFCA3A - false - - Component #8AC926 false + + External Service + #FFCA3A + false + + Flow #1982C4 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 deleted file mode 100644 index 5e21d4a88..000000000 --- a/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllApiLogEntries.listView-meta.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - AllApiLogEntries - NAME - Log__c - LoggingLevel__c - LoggedByUsernameLink__c - Message__c - OriginServiceName__c - Timestamp__c - Everything - - OriginType__c - equals - API - - - diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginServiceName__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml similarity index 84% rename from nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginServiceName__c.field-meta.xml rename to nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml index 993b8562b..1bcce08c0 100644 --- a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginServiceName__c.field-meta.xml +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/ExternalServiceId__c.field-meta.xml @@ -1,10 +1,10 @@ - OriginServiceName__c + ExternalServiceId__c Active None true - + 255 false Confidential 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 7aaffdcf9..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 @@ -935,11 +935,6 @@ LogEntry__c.OriginLocation__c true - - false - LogEntry__c.OriginServiceName__c - true - false LogEntry__c.OriginSourceActionName__c @@ -1255,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 e71684970..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 @@ -617,11 +617,6 @@ LogEntry__c.OriginLocation__c true - - false - LogEntry__c.OriginServiceName__c - true - false LogEntry__c.OriginSourceApiName__c @@ -782,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 index e9abcf950..2a0c7d8e7 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerIntegration.permissionset-meta.xml @@ -1,4 +1,4 @@ - + LoggerRestResource 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 200ec001a..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 @@ -851,11 +851,6 @@ LogEntry__c.OriginLocation__c true - - false - LogEntry__c.OriginServiceName__c - true - false LogEntry__c.OriginSourceActionName__c @@ -1171,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/objects/LogEntryEvent__e/fields/OriginServiceName__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml similarity index 86% rename from nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginServiceName__c.field-meta.xml rename to nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml index 653db4a0e..e01414cbf 100644 --- a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/OriginServiceName__c.field-meta.xml +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/ExternalServiceId__c.field-meta.xml @@ -1,13 +1,13 @@ - OriginServiceName__c + ExternalServiceId__c Active None false false false false - + 255 false Confidential 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/log-management/classes/LogEntryEventHandler_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls index 2516ed170..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, @@ -1436,7 +1440,6 @@ private class LogEntryEventHandler_Tests { Id, Name, OriginLocation__c, - OriginServiceName__c, OriginSourceActionName__c, OriginSourceApiName__c, OriginSourceId__c, @@ -1497,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, @@ -1762,7 +1769,6 @@ private class LogEntryEventHandler_Tests { logEntry.OriginSourceMetadataType__c, 'logEntry.OriginSourceMetadataType__c was not properly set' ); - System.Assert.areEqual(logEntryEvent.OriginServiceName__c, logEntry.OriginServiceName__c, 'logEntry.OriginServiceName__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 index 3eddef234..25b4afabf 100644 --- a/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LoggerRestResource_Tests.cls @@ -224,11 +224,20 @@ private class LoggerRestResource_Tests { 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); @@ -254,14 +263,18 @@ private class LoggerRestResource_Tests { 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.areEqual(resourceServiceNameAttribute.value.stringValue, publishedLogEntryEvent.OriginServiceName__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('API', publishedLogEntryEvent.OriginType__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 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 index c01f6433a..800ee4289 100644 --- 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 @@ -1,5 +1,5 @@ - 61.0 + 62.0 Active From 6523213d43e4f54a1409b60ec50872a466c42fb0 Mon Sep 17 00:00:00 2001 From: GitHub Action Bot Date: Mon, 30 Dec 2024 06:30:00 +0000 Subject: [PATCH 7/8] Created new core unlocked package version --- README.md | 6 +++--- sfdx-project.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95416e0d3..9ea389404 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ The most robust observability solution for Salesforce experts. Built 100% native ## 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` --- diff --git a/sfdx-project.json b/sfdx-project.json index d54a3639f..24ffc41f9 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -207,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", From 6339ad5be4c9cd883f2647c85afe6e16875f903f Mon Sep 17 00:00:00 2001 From: GitHub Action Bot Date: Mon, 30 Dec 2024 06:30:02 +0000 Subject: [PATCH 8/8] Generated updated package.xml manifest file for core unlocked package version --- nebula-logger/core.package.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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