Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Build](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml/badge.svg)](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml)
[![codecov](https://codecov.io/gh/jongpie/NebulaLogger/branch/main/graph/badge.svg?token=1DJPDRM3N4)](https://codecov.io/gh/jongpie/NebulaLogger)
[![skills.sh](https://skills.sh/b/jongpie/NebulaLogger)](https://skills.sh/jongpie/NebulaLogger)

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.

Expand All @@ -25,6 +26,16 @@ The most robust observability solution for Salesforce experts. Built 100% native

---

## AI Agent Skills

Nebula Logger includes [Agent Skills](https://www.skills.sh/docs) that help AI coding agents (Claude Code, GitHub Copilot, Cursor, and others) install, configure, and use Nebula Logger with the recommended patterns.

```bash
npx skills add jongpie/NebulaLogger
```

---

## Features

1. A unified logging tool that supports easily adding log entries across the Salesforce platform, using:
Expand Down
2 changes: 1 addition & 1 deletion nebula-logger/managed-package/sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
"Nebula Logger - Managed Package@4.16.0-spring-25-release": "04t5Y0000015pGtQAI",
"Nebula Logger - Managed Package@4.17.0-summer-25-release": "04tg70000000r5xAAA",
"Nebula Logger - Managed Package@4.18.0-winter-26-release": "04tg700000086RdAAI",
"Nebula Logger - Managed Package@4.19.0-spring-26-release": "04tg700000086RdAAI"
"Nebula Logger - Managed Package@4.19.0-spring-26-release": "04tg7000000GZbJAAW"
}
}
33 changes: 32 additions & 1 deletion scripts/build/create-and-install-package-version.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This script is used to create a new package version for the specified package alias
# It then auto-adds the new package version the files sfdx-project.json and README.md
# It then auto-adds the new package version the files sfdx-project.json, README.md, and the install skill
# Finally, the new package version is installed it into an org, using the specified target username
param ([string]$targetpackagealias, [string]$targetreadme, [string]$targetusername)

Expand Down Expand Up @@ -124,6 +124,29 @@ function Update-README-Package-Version-Id {
((Get-Content -path $targetreadme -Raw) -replace "sf package install --wait 20 --security-type AdminsOnly --package .{0,18}", $sfUnlockedPackageReplacement) | Set-Content -Path $targetreadme -NoNewline
}

function Update-Install-Skill-Package-Version-Id {
param (
$packageVersionId
)

$installSkillPath = "./skills/nebula-logger-install/SKILL.md"
if (-not (Test-Path $installSkillPath)) {
Write-Debug "Install skill not found at $installSkillPath, skipping"
return
}

$packageVersionId = "$packageVersionId".Trim()
# Only the unlocked package IDs are refreshed here - the managed package IDs are updated manually on managed releases.
# The regex only matches the unlocked URLs because the managed URLs include "mgd=true&" between the query string and "p0="
$sandboxUnlockedReplacement = "Sandbox install link: ``https://test.salesforce.com/packaging/installPackage.apexp?p0=$packageVersionId``"
((Get-Content -path $installSkillPath -Raw) -replace "Sandbox install link: ``https:\/\/test.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}``", $sandboxUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline
$productionUnlockedReplacement = "Production install link: ``https://login.salesforce.com/packaging/installPackage.apexp?p0=$packageVersionId``"
((Get-Content -path $installSkillPath -Raw) -replace "Production install link: ``https:\/\/login.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}``", $productionUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline
# The `--wait 20` value disambiguates the unlocked CLI command from the managed one (which uses `--wait 30`)
$sfUnlockedReplacement = "Salesforce CLI: ``sf package install --wait 20 --security-type AdminsOnly --package $packageVersionId``"
((Get-Content -path $installSkillPath -Raw) -replace "Salesforce CLI: ``sf package install --wait 20 --security-type AdminsOnly --package .{0,18}``", $sfUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline
}

function Install-Package-Version {
param (
$packageVersionId
Expand Down Expand Up @@ -156,5 +179,13 @@ Update-README-Package-Version-Id $packageVersionId
npx prettier --write $targetreadme
git add $targetreadme

$installSkillPath = "./skills/nebula-logger-install/SKILL.md"
if (Test-Path $installSkillPath) {
Write-Debug "Adding new package version ID $packageVersionId to $installSkillPath"
Update-Install-Skill-Package-Version-Id $packageVersionId
npx prettier --write $installSkillPath
git add $installSkillPath
}

Write-Debug "Installing new package version ID $packageVersionId for target user $targetusername"
Install-Package-Version $packageVersionId
25 changes: 25 additions & 0 deletions scripts/build/sync-package-version-number.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
$sfdxProjectJsonPath = "./sfdx-project.json"
$packageJsonPath = "./package.json"
$readmeClassPath = "./README.md"
$installSkillPath = "./skills/nebula-logger-install/SKILL.md"
$loggerClassPath = "./nebula-logger/core/main/logger-engine/classes/Logger.cls"
$loggerComponentPath = "./nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js"

Expand Down Expand Up @@ -54,6 +55,29 @@ function Update-README {
git add $readmeClassPath
}

function Get-Install-Skill {
Get-Content -Raw -Path $installSkillPath
}

function Update-Install-Skill {
param (
$versionNumber
)
if (-not (Test-Path $installSkillPath)) {
Write-Output "Install skill not found at $installSkillPath, skipping"
return
}
$versionNumber = "v" + $versionNumber
$installSkillContents = Get-Install-Skill
Write-Output "Bumping install skill unlocked package version number to: $versionNumber"

$targetRegEx = "(### Unlocked Package - )(v[\d\.]+)"
$replacementRegEx = '$1' + $versionNumber
$installSkillContents -replace $targetRegEx, $replacementRegEx | Set-Content -Path $installSkillPath -NoNewline
npx prettier --write $installSkillPath
git add $installSkillPath
}

function Get-Logger-Class {
Get-Content -Raw -Path $loggerClassPath
}
Expand Down Expand Up @@ -97,5 +121,6 @@ Write-Output "Target Version Number: $versionNumber"

Update-Package-JSON $versionNumber
Update-README $versionNumber
Update-Install-Skill $versionNumber
Update-Logger-Class $versionNumber
Update-Logger-Component $versionNumber
31 changes: 31 additions & 0 deletions skills.sh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://skills.sh/schemas/skills.sh.schema.json",
"notGrouped": "bottom",
"groupings": [
{
"title": "Setup",
"description": "Installation and configuration of Nebula Logger in a Salesforce org.",
"skills": ["nebula-logger-install"]
},
{
"title": "Usage",
"description": "Writing logging code across Apex, LWC, Aura, Flow, and OmniStudio, and testing code that uses Nebula Logger.",
"skills": ["nebula-logger-instrumentation", "nebula-logger-testing-your-code"]
},
{
"title": "Operations",
"description": "Day-to-day console navigation and log retention / purging administration.",
"skills": ["nebula-logger-console", "nebula-logger-purging-and-retention"]
},
{
"title": "Governance",
"description": "Best practices, logging levels, and governor limit considerations.",
"skills": ["nebula-logger-best-practices"]
},
{
"title": "Extending",
"description": "Building custom plugins that extend Nebula Logger.",
"skills": ["nebula-logger-plugin-development"]
}
]
}
29 changes: 29 additions & 0 deletions skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Nebula Logger Skills

This folder contains Nebula Logger's [skills.sh](https://skills.sh) catalog. Each subfolder is one skill, with a `SKILL.md` file that describes when the skill applies and what it covers. The top-level [`skills.sh.json`](../skills.sh.json) groups the skills into logical categories for the catalog page at [skills.sh/jongpie/NebulaLogger](https://skills.sh/jongpie/NebulaLogger).

## Skills

| Skill | Purpose |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [nebula-logger-install](./nebula-logger-install/SKILL.md) | Installing Nebula Logger, choosing between unlocked and managed packages, permission-set assignment, and initial `LoggerSettings__c` configuration. |
| [nebula-logger-instrumentation](./nebula-logger-instrumentation/SKILL.md) | Adding Nebula Logger instrumentation in Apex, LWC, Aura, Flow, and OmniStudio. Covers logging APIs, exception handling, record association, tags/scenarios, async transaction linking, and the `CallableLogger` optional-dependency pattern for ISVs. |
| [nebula-logger-console](./nebula-logger-console/SKILL.md) | Browsing and investigating logs in the Salesforce UI - list views, record pages, home page components, and the live log entry event stream. |
| [nebula-logger-purging-and-retention](./nebula-logger-purging-and-retention/SKILL.md) | Configuring how long logs live before being purged - `LogRetentionDate__c` semantics, `LogBatchPurger` scheduling, and per-user/profile/scenario overrides. |
| [nebula-logger-testing-your-code](./nebula-logger-testing-your-code/SKILL.md) | Writing Apex and LWC tests for code that calls Nebula Logger. Global-only APIs and patterns for observing entries without polluting the org. |
| [nebula-logger-plugin-development](./nebula-logger-plugin-development/SKILL.md) | Building custom plugins that extend Nebula Logger via the `LoggerPlugin.Triggerable` and `LoggerPlugin.Batchable` interfaces. |
| [nebula-logger-best-practices](./nebula-logger-best-practices/SKILL.md) | Team-wide instrumentation standards, environment-aware logging levels, and governor-limit considerations. |

## Supported API Surface

Every skill in this catalog references only Nebula Logger's `global` Apex surface and the exported `c/logger` LWC module. `public` classes and methods in the unlocked package are technically reachable but are Nebula Logger's internal surface and can change without a deprecation window - see the "Supported API Surface" section in [nebula-logger-instrumentation](./nebula-logger-instrumentation/SKILL.md) for the full policy.

The plugin framework in [nebula-logger-plugin-development](./nebula-logger-plugin-development/SKILL.md) is a deliberate exception: the extension points are `public` because they exist only inside the unlocked package, and plugin authors interact with them by design. Plugins should pin to a tested Nebula Logger version and re-verify on upgrades.

## Publishing

skills.sh reads this repository directly from GitHub. Landing a change on `main` publishes it - no separate submission or manual crawl trigger is needed.

## Contributing

If you spot something in a skill that no longer matches the code (a renamed field, a removed method, a stale example), please file an issue at https://github.com/jongpie/NebulaLogger/issues or open a PR. See the repo's [CONTRIBUTING.md](../CONTRIBUTING.md) for the general contribution workflow.
78 changes: 78 additions & 0 deletions skills/nebula-logger-best-practices/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
name: nebula-logger-best-practices
description: Use this skill when the user wants to review, harden, or standardize Nebula Logger usage across a Salesforce team. Covers operational logging standards, environment-aware settings, limit-aware design, and governance guardrails.
---

# Nebula Logger Best Practices and Governance

## Team-Wide Practices

Use these defaults when reviewing pull requests or designing logging conventions.

1. Reserve `ERROR`, `WARN`, and `INFO` for information that is operationally significant.
2. Use `DEBUG`, `FINE`, `FINER`, and `FINEST` for high-volume diagnostic detail. Combined with `LoggerSettings__c.LoggingLevel__c`, these can be left in code without adding runtime overhead in production and switched on only when deeper diagnostics are needed.
3. Tune `LoggerSettings__c` data by environment.
- Configure `LoggingLevel__c` to `ERROR`, `WARN`, or `INFO` in production orgs to reduce logging noise (or change to `DEBUG`, `FINE`, `FINER`, or `FINEST` when trying to debug)
- Configure scheduled purging and retention windows (`DefaultLogPurgeAction__c`, `DefaultNumberOfDaysToRetainLogs__c`)
4. Use static method `Logger.setScenario()`, and instance methods `LogEntryEventBuilder.addTag()` and `LogEntryEventBuilder.addTags()` for business-process grouping.
- Define a controlled naming convention for scenarios & tags that make sense to your team
5. Use instance methods on `LogEntryEventBuilder` to further enrich data, instead of embedding extra data directly in message strings. There are several method overloads available:
- `.setExceptionDetails(...)`
- `.setApprovalResult(...)`
- `.setDatabaseResult(...)`
- `.setRecord(...)`
- `.setHttpRequestDetails(...)`
- `.setHttpResponseDetails(...)`
- `.setRestRequestDetails(...)`
- `.setRestResponseDetails(...)`
- `.setField(...)`
6. Call `Logger.saveLog()` deliberately - don't call it after every log entry, and never inside a tight loop. Be strategic when calling it, just like when making DML calls in Apex.
- `Logger.info(...)` / `.error(...)` / etc. only add entries to an in-memory buffer. Nothing persists until `saveLog()` runs. If a transaction ends without calling `saveLog()`, everything that was buffered is lost - so `saveLog()` still has to be called before the transaction commits.
- Every `saveLog()` call is a real platform operation with real cost. With the default `EVENT_BUS` save method, each call to `saveLog()` calls `System.EventBus.publish(List<LogEntryEvent__e>)` once, which consumes one slot against `System.Limits.getLimitPublishImmediateDML()` (100 per transaction) and one increment against the org's daily platform event publish allocation. The other save methods have their own limits: `QUEUEABLE` consumes an async job slot (`System.Limits.getLimitQueueableJobs()`), `REST` consumes a callout, and `SYNCHRONOUS_DML` consumes regular DML rows and statements. None of them are free.
- Multiple `saveLog()` calls in a transaction are fine, and often the right choice. Reasonable places to save intermediate state include after each chunk in a batch, at the end of each iteration of a long-running loop's outer scope (never the inner scope - see below), before an async handoff (Queueable / Future / Batchable), and inside a `finally` block that catches an exception you're about to rethrow. Each save publishes only what's in the buffer at that moment, so the entries persist even if the rest of the transaction later blows up.
- What's wrong is calling `saveLog()` after every single log entry. That turns N log calls into N platform-event publishes, burns through `getLimitPublishImmediateDML()` (100 per transaction) fast, and eats into the org's daily platform event allocation for no operational benefit. If you find yourself typing `Logger.info(...); Logger.saveLog();` repeatedly, buffer the entries and save once after the group.
- Never call `saveLog()` inside the innermost body of a loop over records. Buffer entries in the loop and call `saveLog()` after the loop (or at safe checkpoints - after N iterations, after a chunk of work, etc.), not after each record.
- Place a `Logger.saveLog()` call in a `finally` block for transactional code paths so it still runs when an exception escapes the try. Pair it with `Logger.setSaveMethod(...)` at the top of the method if the default `EVENT_BUS` isn't right for that path (see the save-method notes in item 1).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"item 1" has changed due to this commit. Thus, this should be adjusted.


## Environment-Aware Logging Levels

Treat logging verbosity as an operational control, not as a code constant.

| Environment | Suggested baseline | Reason |
| ----------- | ------------------------------- | ---------------------------------------------------------------- |
| Production | `ERROR` or `WARN` | Reduce noise and storage impact while preserving incident signal |
| UAT | `INFO` | Validate business flows without excessive detail |
| QA | `DEBUG` or `FINE` | Support defect reproduction and integration testing |
| Sandbox/Dev | `FINE` to `FINEST` (time-boxed) | Deep diagnostics during active development |

Use hierarchy overrides (user/profile) for temporary incident windows, then revert.

## Review Checklist for Existing Code

- Does each execution path call `saveLog()` at deliberate checkpoints - and never after every entry or inside a per-record inner loop?
- Are exceptions logged with stack trace context?
- Are record references attached via `.setRecord(...)`?
- Are scenarios and tags consistent with team taxonomy?
- Is save method choice explicit when not using default `EVENT_BUS`?

## Governor and Capacity Considerations

Nebula Logger helps observability, but it still runs inside Salesforce governor boundaries.

| Constraint | Impact on logging | Mitigation |
| -------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Data storage | High-volume logs can consume custom object storage quickly | Enforce retention policy and purge schedule; lower verbosity in production |
| Platform event daily limit | `EVENT_BUS` volume can hit org event allocations | Use selective logging and switch targeted jobs to `QUEUEABLE`/`SYNCHRONOUS_DML` when needed |
| SOQL queries | Extra enrichment queries in logging paths can compound limits | Reuse queried records and avoid logging-only query fanout |
| CPU time | Heavy serialization/tagging in loops increases CPU usage | Log aggregate milestones, not every iteration, and prefer async save paths |
| Async job allocation | Overusing `QUEUEABLE` can compete with business async work | Reserve queueable saves for high-cost or mixed-DML-sensitive contexts |
| Heap size | Large payloads/exceptions can inflate transaction memory | Truncate oversized payloads and avoid dumping full object graphs |

## When Not to Use Nebula Logger

Nebula Logger is not a substitute for sound architecture.

- Do not use logging as a workaround for unclear domain design.
- Do not store secrets, credentials, or unnecessary PII in log messages.

Use Nebula Logger as a structured observability layer, combined with clean service boundaries, resilient error handling, and purposeful telemetry.
Loading