Merge main into activity registry optimization branch
This commit is contained in:
commit
a3d5fbc133
126
.agents/skills/elsa-roadmap-refresh/SKILL.md
Normal file
126
.agents/skills/elsa-roadmap-refresh/SKILL.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
name: elsa-roadmap-refresh
|
||||
description: Refresh the Elsa roadmap from current evidence. Use when Codex needs to update ROADMAP.md or the public roadmap issue by researching elsa-core, elsa-studio, and elsa-extensions source code, specs, releases, commits, issues, pull requests, and discussions; classify shipped foundations, partial/productization work, roadmap candidates, stale items, and recommendations; then update the roadmap checklist and GitHub issue coherently.
|
||||
---
|
||||
|
||||
# Elsa Roadmap Refresh
|
||||
|
||||
## Purpose
|
||||
|
||||
Use this skill to refresh Elsa's roadmap from evidence across the Elsa product surface:
|
||||
|
||||
- `elsa-workflows/elsa-core`
|
||||
- `elsa-workflows/elsa-studio`
|
||||
- `elsa-workflows/elsa-extensions`
|
||||
|
||||
The output should be a coherent product roadmap, not a raw issue digest. Ground every substantial change in source code, specs, releases, issues, pull requests, or discussions.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Before editing, run `git status --short` in `elsa-core`.
|
||||
- Never revert unrelated user changes.
|
||||
- If `ROADMAP.md` or `README.md` already has uncommitted user edits, stop and report the conflict unless the user explicitly asked to work through it.
|
||||
- Edit only roadmap-related files unless the user asks for more.
|
||||
- Do not commit, push, or publish unless the user or automation prompt explicitly asks for it.
|
||||
- If updating GitHub issue `elsa-core#3232`, use the final `ROADMAP.md` body as the issue body.
|
||||
|
||||
## Evidence Workflow
|
||||
|
||||
1. Read the current roadmap.
|
||||
- `ROADMAP.md`
|
||||
- `README.md` roadmap link, if relevant
|
||||
- GitHub issue `elsa-workflows/elsa-core#3232`
|
||||
|
||||
2. Inspect `elsa-core`.
|
||||
- Source modules under `src/modules`
|
||||
- App hosts under `src/apps`
|
||||
- Specs under `specs`
|
||||
- Docs under `doc` and `design`
|
||||
- Recent releases, commits, open issues, discussions, and open PRs
|
||||
|
||||
3. Inspect `elsa-studio`.
|
||||
- Source layout and feature modules
|
||||
- Designer, diagnostics, auth, localization, embedding, workflow authoring, and instance UX
|
||||
- Recent releases, commits, open issues, discussions, and open PRs
|
||||
|
||||
4. Inspect `elsa-extensions`.
|
||||
- Module families and integration foundations
|
||||
- Connections, Secrets, Agents, OpenAPI, schedulers, messaging, persistence, data tooling, and package manifests
|
||||
- Recent releases, commits, open issues, and open PRs
|
||||
- Discussions if enabled; otherwise state that issue/PR signal is the available source
|
||||
|
||||
Use `gh` for GitHub data when available. Prefer local clones when present; otherwise use temporary clones or `gh repo clone` into `/tmp`.
|
||||
|
||||
## Classification Rules
|
||||
|
||||
Use these categories consistently:
|
||||
|
||||
- `[x]` shipped foundation: implemented in source or released enough to build on.
|
||||
- `[~]` partially shipped or needs productization: code exists, but docs, Studio UX, integration, tests, release packaging, or operational readiness are incomplete.
|
||||
- `[ ]` roadmap candidate: valuable capability not yet implemented or only represented by issue/PR/proposal.
|
||||
- stale/obsolete: roadmap item is already done, superseded, or no longer matches repository direction.
|
||||
|
||||
Be explicit when a conclusion is an inference from evidence rather than a direct source fact.
|
||||
|
||||
## Roadmap Editing Rules
|
||||
|
||||
- Keep `ROADMAP.md` product-oriented and exciting, but technically defensible.
|
||||
- Preserve the main structure unless a better structure is clearly needed:
|
||||
- North Star
|
||||
- Capability Checklist
|
||||
- Current Foundations
|
||||
- Production Confidence
|
||||
- Authoring Productivity
|
||||
- Integrations And Ecosystem
|
||||
- Observability And Operations
|
||||
- Security, Identity, And Enterprise Readiness
|
||||
- AI-Assisted Workflow Engineering
|
||||
- Recommended Sequencing
|
||||
- Maintainership Recommendations
|
||||
- Update `Last refreshed` to the current date.
|
||||
- Keep issue and PR links close to the claims they support.
|
||||
- Prefer platform tracks over long lists of unrelated features.
|
||||
- Separate what is already present from what still needs productization.
|
||||
- Do not overpromise release dates.
|
||||
|
||||
## High-Value Themes To Re-Evaluate Each Refresh
|
||||
|
||||
- Runtime recovery, graceful shutdown, distributed execution, scheduler/message reliability
|
||||
- Studio designer reliability, state machine authoring, debugging, progress/timeline, embedded components
|
||||
- Studio diagnostics, structured logs, console logs, OpenTelemetry, workflow incident timelines
|
||||
- Extension Platform, package manifests, Connections, Secrets, generated activities, connector SDK
|
||||
- Agents, MCP/tooling, provider matrix, AI-assisted authoring
|
||||
- OpenAPI activity provider, Azure Functions, Azure DevOps, Microsoft 365 and Google Workspace integrations
|
||||
- Security, OIDC, tenant/role activity visibility, localization, white-label readiness
|
||||
- Marketplace/plugin installation path and package maturity states
|
||||
|
||||
## Recommended GitHub Commands
|
||||
|
||||
```bash
|
||||
gh issue view 3232 --repo elsa-workflows/elsa-core --json title,body,updatedAt,url
|
||||
gh issue list --repo elsa-workflows/elsa-core --state open --limit 200 --json number,title,labels,updatedAt,url
|
||||
gh issue list --repo elsa-workflows/elsa-studio --state open --limit 200 --json number,title,labels,updatedAt,url
|
||||
gh issue list --repo elsa-workflows/elsa-extensions --state open --limit 200 --json number,title,labels,updatedAt,url
|
||||
gh pr list --repo elsa-workflows/elsa-core --state open --limit 100 --json number,title,updatedAt,url
|
||||
gh pr list --repo elsa-workflows/elsa-studio --state open --limit 100 --json number,title,updatedAt,url
|
||||
gh pr list --repo elsa-workflows/elsa-extensions --state open --limit 100 --json number,title,updatedAt,url
|
||||
gh release list --repo elsa-workflows/elsa-core --limit 10
|
||||
gh release list --repo elsa-workflows/elsa-studio --limit 10
|
||||
gh release list --repo elsa-workflows/elsa-extensions --limit 10
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
When asked to mirror the roadmap to GitHub:
|
||||
|
||||
```bash
|
||||
gh issue edit 3232 --repo elsa-workflows/elsa-core --body-file ROADMAP.md
|
||||
```
|
||||
|
||||
After publishing, verify:
|
||||
|
||||
```bash
|
||||
gh issue view 3232 --repo elsa-workflows/elsa-core --json title,url,updatedAt
|
||||
```
|
||||
|
||||
Final response should summarize changed themes, whether the GitHub issue was updated, and any files left modified.
|
||||
4
.agents/skills/elsa-roadmap-refresh/agents/openai.yaml
Normal file
4
.agents/skills/elsa-roadmap-refresh/agents/openai.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
interface:
|
||||
display_name: "Elsa Roadmap Refresh"
|
||||
short_description: "Refresh the Elsa roadmap from source and GitHub evidence"
|
||||
default_prompt: "Use $elsa-roadmap-refresh to update ROADMAP.md and mirror it to the public roadmap issue."
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"feature_directory": "specs/007-secrets-module"
|
||||
"feature_directory": "specs/010-workflow-json-hardening"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ Before handing off changes, verify the following when applicable:
|
|||
|
||||
<!-- SPECKIT START -->
|
||||
For additional context about technologies to be used, project structure,
|
||||
shell commands, and other important information, read `specs/007-secrets-module/plan.md`.
|
||||
shell commands, and other important information, read `specs/010-workflow-json-hardening/plan.md`.
|
||||
<!-- SPECKIT END -->
|
||||
|
||||
## Active Technologies
|
||||
|
|
|
|||
|
|
@ -99,8 +99,10 @@
|
|||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8"/>
|
||||
<PackageVersion Include="Bogus" Version="35.6.5"/>
|
||||
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.Core" Version="1.0.0"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.SignalR" Version="1.0.0"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.Contracts" Version="1.0.0-preview.13"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.Core" Version="1.0.0-preview.13"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.Persistence.Sqlite" Version="1.0.0-preview.13"/>
|
||||
<PackageVersion Include="ConsoleLogStreaming.SignalR" Version="1.0.0-preview.13"/>
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All"/>
|
||||
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Cronos" Version="0.11.1"/>
|
||||
|
|
@ -153,13 +155,13 @@
|
|||
<PackageVersion Include="Nuplane.Sources.Directory" Version="0.0.8"/>
|
||||
<PackageVersion Include="Nuke.Components" Version="10.1.0"/>
|
||||
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0"/>
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.15.3"/>
|
||||
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.13.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3"/>
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.14.0-beta.1"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.14.0-beta.1"/>
|
||||
<PackageVersion Include="Parlot" Version="1.5.7"/>
|
||||
|
|
|
|||
49
Elsa.sln
49
Elsa.sln
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.7.34003.232
|
||||
|
|
@ -347,6 +347,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.UnitTests", "t
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.Api.UnitTests", "test\unit\Elsa.Workflows.Api.UnitTests\Elsa.Workflows.Api.UnitTests.csproj", "{81CFD2E0-2E5E-4810-ADB8-A08301199166}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry", "src\modules\Elsa.Diagnostics.OpenTelemetry\Elsa.Diagnostics.OpenTelemetry.csproj", "{8F4AD54E-8586-4D8C-82E6-69218DD4280F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.UnitTests", "test\unit\Elsa.Diagnostics.OpenTelemetry.UnitTests\Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj", "{60F844CC-6DA6-45EB-BA5E-B480B4E77624}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.IntegrationTests", "test\integration\Elsa.Diagnostics.OpenTelemetry.IntegrationTests\Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj", "{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1387,6 +1393,42 @@ Global
|
|||
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x64.Build.0 = Release|Any CPU
|
||||
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x64.Build.0 = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1504,12 +1546,15 @@ Global
|
|||
{A0D906D7-9E4D-4C50-93B4-8720BB0AAFA7} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
|
||||
{695814F0-7E8F-469E-9A5A-E46759A4D67C} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{FB7836E3-1D05-4123-B4FA-B9E722723603} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{195FD304-EC3F-4350-93C8-AFE80C4E6896} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
|
||||
{D8739449-22DC-42D4-85A4-4BA547B0B458} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{93E9213A-694D-4AB4-870E-05E44F793133} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{81CFD2E0-2E5E-4810-ADB8-A08301199166} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{195FD304-EC3F-4350-93C8-AFE80C4E6896} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
|
||||
{8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{60F844CC-6DA6-45EB-BA5E-B480B4E77624} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="cshells-feedz" value="https://f.feedz.io/sfmskywalker/cshells/nuget/index.json" />
|
||||
<add key="elsa-preview-feedz" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
|
||||
<add key="valence-consolelogstream-feedz" value="https://f.feedz.io/valence-works/consolelogstream/nuget/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="NuGet official package source">
|
||||
|
|
@ -19,5 +20,9 @@
|
|||
<package pattern="Elsa.PackageManifest.Generator" />
|
||||
<package pattern="Elsa.PackageManifests" />
|
||||
</packageSource>
|
||||
<packageSource key="valence-consolelogstream-feedz">
|
||||
<package pattern="ConsoleLogStreaming" />
|
||||
<package pattern="ConsoleLogStreaming.*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
|
|
|||
60
ROADMAP.md
60
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
|||
# Elsa Roadmap
|
||||
|
||||
Last refreshed: 2026-05-19
|
||||
Last refreshed: 2026-05-29
|
||||
|
||||
This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem.
|
||||
|
||||
|
|
@ -38,13 +38,13 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio
|
|||
- [x] Distributed runtime package
|
||||
- [x] Structured logs
|
||||
- [x] Console logs
|
||||
- [x] Studio structured-log and console-log foundations
|
||||
- [x] Studio structured-log, console-log, and OpenTelemetry diagnostics foundations
|
||||
- [~] Durable structured log persistence
|
||||
- [~] Scheduler and message-bus foundations through Quartz, Hangfire, MassTransit, Kafka, and Azure Service Bus
|
||||
- [ ] OpenTelemetry traces and default workflow metrics
|
||||
- [~] OpenTelemetry diagnostics backend and default workflow metrics
|
||||
- [ ] Scheduler/message reliability hardening for clustered production workloads
|
||||
- [ ] Production security guide
|
||||
- [ ] Enterprise deployment checklist
|
||||
- [ ] Enterprise deployment platform and checklist
|
||||
|
||||
### Authoring And Studio
|
||||
|
||||
|
|
@ -56,8 +56,8 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio
|
|||
- [~] React Flow, sequence, and state-machine designer foundations
|
||||
- [~] Workflow organization with labels/categories/folders
|
||||
- [~] Workflow progress/timeline surface
|
||||
- [~] Studio OIDC and identity modules
|
||||
- [~] Studio diagnostics pages for structured and console logs
|
||||
- [x] Studio OIDC and identity modules
|
||||
- [~] Studio diagnostics pages for structured logs, console logs, and OpenTelemetry
|
||||
- [~] Studio alterations module
|
||||
- [ ] Designer reliability/regression hardening
|
||||
- [ ] Input validation and activity version visibility
|
||||
|
|
@ -66,7 +66,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio
|
|||
- [ ] First-class workflow debugging
|
||||
- [ ] Designer extensibility, embedding, and white-label recipes
|
||||
- [ ] Tenant/role-based activity visibility
|
||||
- [ ] AI-assisted workflow generation
|
||||
- [~] AI-assisted workflow generation and copilot foundations
|
||||
|
||||
### Integrations
|
||||
|
||||
|
|
@ -77,11 +77,11 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio
|
|||
- [~] OpenAPI activity/provider foundations
|
||||
- [ ] Connector SDK
|
||||
- [ ] Marketplace/plugin installation
|
||||
- [ ] Agent provider matrix and MCP/tool lifecycle
|
||||
- [~] Agent provider matrix, with MCP/tool lifecycle still roadmap
|
||||
- [ ] Dynamic activity generation from OpenAPI, Azure Functions, registered methods, and schemas
|
||||
- [ ] Azure DevOps, Teams, OneDrive, SharePoint, Google Docs, and Google Sheets integration strategy
|
||||
- [ ] SQL authoring quality: Studio drag/drop reliability and IntelliSense
|
||||
- [ ] Dapper package split and MongoDB secrets parity
|
||||
- [~] Dapper package split and MongoDB secrets parity
|
||||
- [ ] MassTransit v9 strategy
|
||||
- [ ] Azure Functions / worker-service hosting guidance
|
||||
- [ ] WatchFileSystem and command-line automation activities
|
||||
|
|
@ -93,21 +93,24 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio
|
|||
These are already present in the codebase and should be treated as foundations for the next roadmap slices:
|
||||
|
||||
- Multi-targeting for `net8.0`, `net9.0`, and `net10.0` in [`src/Directory.Build.props`](src/Directory.Build.props).
|
||||
- The `3.7.0` release train shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.0) in May 2026, promoting shell integration, Studio authentication, workflow diagnostics, and extension package metadata into released foundations.
|
||||
- Modular core packages under [`src/modules`](src/modules), with code-first features and CShells shell features documented in [`doc/wiki/module-system.md`](doc/wiki/module-system.md).
|
||||
- A modular server host using CShells and Nuplane package loading in [`src/apps/Elsa.ModularServer.Web`](src/apps/Elsa.ModularServer.Web).
|
||||
- Runtime admin, quiescence, drain, and interrupted recovery infrastructure in [`Elsa.Workflows.Runtime`](src/modules/Elsa.Workflows.Runtime) and runtime admin endpoints in [`Elsa.Workflows.Api`](src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin).
|
||||
- Distributed runtime support in [`Elsa.Workflows.Runtime.Distributed`](src/modules/Elsa.Workflows.Runtime.Distributed).
|
||||
- Structured diagnostics with recent/live capture plus SQLite persistence in [`Elsa.Diagnostics.StructuredLogs`](src/modules/Elsa.Diagnostics.StructuredLogs) and [`Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite`](src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite).
|
||||
- Raw stdout/stderr console diagnostics in [`Elsa.Diagnostics.ConsoleLogs`](src/modules/Elsa.Diagnostics.ConsoleLogs).
|
||||
- Raw stdout/stderr console diagnostics in [`Elsa.Diagnostics.ConsoleLogs`](src/modules/Elsa.Diagnostics.ConsoleLogs), with the post-3.7 console pipeline now carrying workflow and activity execution context through [PR #7536](https://github.com/elsa-workflows/elsa-core/pull/7536).
|
||||
- Core OpenTelemetry diagnostics are actively in productization through [PR #7537](https://github.com/elsa-workflows/elsa-core/pull/7537), which adds OTLP ingestion, bounded storage, REST APIs, SignalR live updates, collector configuration, security checks, and tests.
|
||||
- State machine core activity support in [`Elsa.Workflows.Core/Activities/StateMachine`](src/modules/Elsa.Workflows.Core/Activities/StateMachine).
|
||||
- ElsaScript DSL and blob storage integration in [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) and [`Elsa.WorkflowProviders.BlobStorage.ElsaScript`](src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript).
|
||||
- Activity unit testing helpers and guidance in [`src/common/Elsa.Testing.Shared`](src/common/Elsa.Testing.Shared) and [`doc/qa/test-guidelines.md`](doc/qa/test-guidelines.md).
|
||||
- Label infrastructure in [`Elsa.Labels`](src/modules/Elsa.Labels), which is the likely backend foundation for workflow categories, tags, and folders.
|
||||
- Elsa Studio is already a modular Blazor product shell with workflow authoring, instance browsing, designer modules, diagnostics, authentication, localization, branding, custom elements, and early React wrapper work in [elsa-workflows/elsa-studio](https://github.com/elsa-workflows/elsa-studio).
|
||||
- Studio `main` includes structured-log and console-log modules, newer React Flow/sequence/state-machine designer work, OIDC/identity infrastructure, custom elements, and an alterations module.
|
||||
- Studio `3.7.0` shipped the modern authentication framework, Elsa Identity and OIDC modules, activity call-stack visualization, incident count badges, pending-instance filtering, and custom theme/DataPanel extensibility.
|
||||
- Studio `main` includes structured-log, console-log, and OpenTelemetry diagnostics modules, newer React Flow/sequence/state-machine designer work, OIDC/identity infrastructure, custom elements, and an alterations module. The OpenTelemetry Studio module landed in [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834).
|
||||
- Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`.
|
||||
- Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, and external system activities.
|
||||
- Recent extension work adds package manifest metadata, Kafka multitenancy/schema triggers, Quartz lifecycle/job cleanup fixes, Dapper bookmark cleanup, and other operational hardening.
|
||||
- Extensions `3.7.0` adds package manifest metadata, infrastructure attributes, shell features for MassTransit/Quartz/Webhooks, Dapper and MongoDB activity execution-chain lookups, Dapper bookmark queue filtering, Kafka multitenancy/schema-trigger work, Quartz lifecycle/job cleanup fixes, and other operational hardening.
|
||||
|
||||
The public roadmap issue remains useful history: [elsa-workflows/elsa-core#3232](https://github.com/elsa-workflows/elsa-core/issues/3232). Several items in that issue are now done in code but still open in the issue body, so this file should be considered the current working roadmap.
|
||||
|
||||
|
|
@ -123,12 +126,14 @@ High-value items:
|
|||
- Treat scheduler and messaging correctness as release-blocking infrastructure. Extensions issues around Quartz clustering and recovery ([elsa-extensions#109](https://github.com/elsa-workflows/elsa-extensions/issues/109), [elsa-extensions#101](https://github.com/elsa-workflows/elsa-extensions/issues/101)), Hangfire duplicate jobs ([elsa-extensions#121](https://github.com/elsa-workflows/elsa-extensions/issues/121)), MassTransit stimulus routing ([elsa-extensions#72](https://github.com/elsa-workflows/elsa-extensions/issues/72)), and Kafka extensibility ([elsa-extensions#134](https://github.com/elsa-workflows/elsa-extensions/issues/134)) all point to the same production theme: clustered workload behavior must be boring, observable, and customizable.
|
||||
- Turn the draft native background execution architecture into an implementation plan. [#7356](https://github.com/elsa-workflows/elsa-core/issues/7356) and [#7313](https://github.com/elsa-workflows/elsa-core/issues/7313) point toward an engine-owned, workflow-aware runtime that can evolve toward an actor-model abstraction without coupling Elsa to Orleans, Proto.Actor, or any single backend.
|
||||
- Treat persistence and migration reliability as a product feature: provider-specific migration validation, large-tenant performance tests, safer defaults, and upgrade notes that cover SQL Server, PostgreSQL, MySQL, SQLite, Oracle, and MongoDB scenarios.
|
||||
- Promote the Elsa Deployment Platform PRD into scoped implementation work. [#7469](https://github.com/elsa-workflows/elsa-core/issues/7469) defines the right product boundary: declarative environment manifests, immutable deployment artifacts, dry-run validation, deployment history, and GitOps-compatible reconciliation should manage control-plane state without reconciling runtime execution state.
|
||||
- Maintain a security hardening track: document scripting trust boundaries, production-safe Docker posture, OIDC setup, default auth patterns, and secrets redaction. [#7096](https://github.com/elsa-workflows/elsa-core/issues/7096) is a reminder that optional code execution must be explained and guarded clearly.
|
||||
|
||||
Recommended success measures:
|
||||
|
||||
- rolling deploys do not leave workflows ambiguous or unrecoverable
|
||||
- every runtime admin action is observable and auditable
|
||||
- deployments can be previewed, validated, promoted, rolled back, and audited without copying databases or reconciling runtime state
|
||||
- core distributed scenarios have component tests, not only unit coverage
|
||||
- upgrade guides call out migrations, package renames, and production security implications before users hit them
|
||||
|
||||
|
|
@ -142,7 +147,7 @@ High-value items:
|
|||
- Make designer reliability a visible workstream. Recent Studio issues show expression/input rendering regressions after 3.6 ([elsa-studio#791](https://github.com/elsa-workflows/elsa-studio/issues/791), [elsa-studio#781](https://github.com/elsa-workflows/elsa-studio/issues/781), [elsa-studio#795](https://github.com/elsa-workflows/elsa-studio/issues/795)); these should drive a regression harness for designer rendering, property editors, expression descriptors, drag/drop, and WASM/Server parity.
|
||||
- Make workflow progress visible to application users: a current-state/step API, timeline model, and embeddable progress component. This responds to [discussion #6012](https://github.com/elsa-workflows/elsa-core/discussions/6012) and should reuse execution logs, activity records, call-stack tracking, and real-time workflow updates.
|
||||
- Finish the state machine product surface. The core activity exists, but [#5085](https://github.com/elsa-workflows/elsa-core/issues/5085) should be closed only when JSON serialization, Studio authoring, docs, and examples make state machines approachable.
|
||||
- Build first-class workflow testing and debugging: test runners for full workflows, breakpoint-like inspection, replay from execution logs where feasible, better failed-activity retry flows, child/descendant workflow instance navigation, and Studio affordances for fault investigation. This expands the activity test helper work that addressed [#3978](https://github.com/elsa-workflows/elsa-core/issues/3978) and matches Studio requests for child workflow visibility ([elsa-studio#152](https://github.com/elsa-workflows/elsa-studio/issues/152)) and breakpoint debugging ([elsa-studio discussion #662](https://github.com/elsa-workflows/elsa-studio/discussions/662)).
|
||||
- Build first-class workflow testing and debugging: test runners for full workflows, breakpoint-like inspection, replay from execution logs where feasible, better failed-activity retry flows, child/descendant workflow instance navigation, and Studio affordances for fault investigation. The Studio `3.7.0` activity call-stack viewer is a useful foundation, but requests for child workflow visibility ([elsa-studio#152](https://github.com/elsa-workflows/elsa-studio/issues/152)) and breakpoint debugging ([elsa-studio discussion #662](https://github.com/elsa-workflows/elsa-studio/discussions/662)) still need a coherent debugging experience.
|
||||
- Improve Studio authoring fundamentals: input validation ([elsa-studio#15](https://github.com/elsa-workflows/elsa-studio/issues/15)), activity version indicators ([elsa-studio#284](https://github.com/elsa-workflows/elsa-studio/issues/284)), async `/dispatch` instead of blocking `/execute` where appropriate ([elsa-studio#811](https://github.com/elsa-workflows/elsa-studio/issues/811)), designer image export ([elsa-studio#585](https://github.com/elsa-workflows/elsa-studio/issues/585)), and expression evaluation controls ([elsa-studio#643](https://github.com/elsa-workflows/elsa-studio/issues/643)).
|
||||
- Improve designer extensibility, embedding, and white-labeling: custom activity property editors, custom list actions, embeddable designer/viewer recipes, custom elements, React wrapper direction, auth modes, base-path hosting, branding/theme, and clear Blazor/WASM/Server guidance. Community demand appears in [#4743](https://github.com/elsa-workflows/elsa-core/issues/4743), [#6685](https://github.com/elsa-workflows/elsa-core/issues/6685), [discussion #7246](https://github.com/elsa-workflows/elsa-core/discussions/7246), [elsa-studio#137](https://github.com/elsa-workflows/elsa-studio/issues/137), and [elsa-studio discussion #665](https://github.com/elsa-workflows/elsa-studio/discussions/665).
|
||||
- Ship user preference and UI state persistence as a Studio platform feature. [elsa-studio#703](https://github.com/elsa-workflows/elsa-studio/issues/703) already scopes theme, layout, table state, local/session storage, reset behavior, and future server profile storage.
|
||||
|
|
@ -166,10 +171,10 @@ High-value items:
|
|||
- Create an OpenAPI activity provider that turns an OpenAPI document into typed designer activities. This is a recurring ask in [#2961](https://github.com/elsa-workflows/elsa-core/issues/2961) and [#6360](https://github.com/elsa-workflows/elsa-core/issues/6360), and it is the best foundation for a broad connector story. The existing Extensions OpenAPI work should be reconciled with this product goal.
|
||||
- Define a connector SDK before adding many one-off integrations. The SDK should cover authentication, secrets, generated activities, testing, metadata, versioning, packaging, Studio property editors, and manifest-driven installation.
|
||||
- Continue marketplace and plugin infrastructure. The Nuplane modular server, sample package, extension package manifest metadata, and [#7310](https://github.com/elsa-workflows/elsa-core/issues/7310) point to a compelling future where Elsa distributions can install safe custom modules without users maintaining a separate host app.
|
||||
- Promote Agents to a strategic integration lane: provider matrix, OpenAI and Claude PR resolution, local/OpenRouter/custom endpoints, MCP/tool lifecycle, tool selection in Studio, and auditability. [elsa-extensions#58](https://github.com/elsa-workflows/elsa-extensions/issues/58), [elsa-extensions#98](https://github.com/elsa-workflows/elsa-extensions/pull/98), and [elsa-extensions#63](https://github.com/elsa-workflows/elsa-extensions/pull/63) show both demand and active work.
|
||||
- Promote Agents to a strategic integration lane: provider matrix, OpenAI and Claude PR resolution, local/OpenRouter/custom endpoints, MCP/tool lifecycle, tool selection in Studio, and auditability. The Extensions source already contains Agents, OpenAI, Azure OpenAI, persistence, API, and Studio modules, while [elsa-extensions#58](https://github.com/elsa-workflows/elsa-extensions/issues/58), [elsa-extensions#98](https://github.com/elsa-workflows/elsa-extensions/pull/98), and [elsa-extensions#63](https://github.com/elsa-workflows/elsa-extensions/pull/63) show the provider matrix and Claude/OpenAI work are still actively moving.
|
||||
- Prioritize enterprise productivity integrations by leverage: Azure DevOps has a concrete implementation path ([elsa-extensions#124](https://github.com/elsa-workflows/elsa-extensions/issues/124), [elsa-extensions#125](https://github.com/elsa-workflows/elsa-extensions/pull/125)); Teams, OneDrive, SharePoint, Google Docs, and Google Sheets should follow a shared connector model rather than separate bespoke designs.
|
||||
- Treat dynamic activity generation as a platform primitive. Azure Functions ([elsa-extensions#39](https://github.com/elsa-workflows/elsa-extensions/issues/39)) and registered-method activities ([elsa-extensions#48](https://github.com/elsa-workflows/elsa-extensions/issues/48)) both point toward schema/method-driven activity generation that can also serve OpenAPI, SDK-generated connectors, and internal enterprise APIs.
|
||||
- Improve data and automation authoring quality: SQL drag/drop reliability ([elsa-extensions#79](https://github.com/elsa-workflows/elsa-extensions/issues/79)), SQL IntelliSense ([elsa-extensions#88](https://github.com/elsa-workflows/elsa-extensions/issues/88)), Dapper package splitting ([elsa-extensions#131](https://github.com/elsa-workflows/elsa-extensions/issues/131), [elsa-extensions#132](https://github.com/elsa-workflows/elsa-extensions/pull/132)), MongoDB secrets parity ([elsa-extensions#126](https://github.com/elsa-workflows/elsa-extensions/issues/126)), WatchFileSystem ([elsa-extensions#90](https://github.com/elsa-workflows/elsa-extensions/issues/90)), and command-line activities ([elsa-extensions#36](https://github.com/elsa-workflows/elsa-extensions/issues/36)).
|
||||
- Improve data and automation authoring quality: SQL result typing ([elsa-extensions#153](https://github.com/elsa-workflows/elsa-extensions/issues/153), [elsa-extensions#154](https://github.com/elsa-workflows/elsa-extensions/pull/154)), SQL drag/drop reliability ([elsa-extensions#79](https://github.com/elsa-workflows/elsa-extensions/issues/79)), SQL IntelliSense ([elsa-extensions#88](https://github.com/elsa-workflows/elsa-extensions/issues/88)), Dapper package splitting ([elsa-extensions#131](https://github.com/elsa-workflows/elsa-extensions/issues/131), [elsa-extensions#132](https://github.com/elsa-workflows/elsa-extensions/pull/132)), MongoDB secrets parity ([elsa-extensions#126](https://github.com/elsa-workflows/elsa-extensions/issues/126)), WatchFileSystem ([elsa-extensions#90](https://github.com/elsa-workflows/elsa-extensions/issues/90)), and command-line activities ([elsa-extensions#36](https://github.com/elsa-workflows/elsa-extensions/issues/36)).
|
||||
- Resolve the MassTransit strategy after the v9 licensing change. [discussion #6583](https://github.com/elsa-workflows/elsa-core/discussions/6583) raises a practical ecosystem risk; Elsa should either provide a clean split or reduce dependency weight through a smaller messaging abstraction.
|
||||
- Clarify Azure Functions and worker-service hosting patterns. [discussion #4707](https://github.com/elsa-workflows/elsa-core/discussions/4707) and [discussion #7420](https://github.com/elsa-workflows/elsa-core/discussions/7420) show demand for non-traditional hosts, Windows services, and serverless-adjacent deployments.
|
||||
- Add data movement and streaming workflow primitives. [#4809](https://github.com/elsa-workflows/elsa-core/issues/4809) frames this as datasets, linked services, transforms, and stream-oriented processing inspired by Azure Data Factory and stream analytics.
|
||||
|
|
@ -188,9 +193,9 @@ Recommended success measures:
|
|||
|
||||
High-value items:
|
||||
|
||||
- Finish the diagnostics trilogy: structured logs, console logs, and an explicit OpenTelemetry boundary. Structured and console logs now exist; OpenTelemetry traces/metrics need a current module story because [PR #5810](https://github.com/elsa-workflows/elsa-core/pull/5810) previously introduced a module, [#5988](https://github.com/elsa-workflows/elsa-core/issues/5988) asks for default metrics, and the current repo no longer contains an `Elsa.OpenTelemetry` module.
|
||||
- Add default workflow semantic metrics: started, resumed, suspended, faulted, completed, active, activity executed/faulted, queue depth, recovery count, drain count, and dispatch latency. Align with OpenTelemetry semantic convention work where possible.
|
||||
- Build Studio diagnostics pages that are useful under pressure: live console, structured logs, trace/metric links, workflow incident timelines, source health, dropped-event counters, source selection, filters, URL state, export/copy affordances, and direct deep links to workflow instances.
|
||||
- Finish the diagnostics trilogy: structured logs, console logs, and OpenTelemetry. Structured and console logs now exist; Studio `main` has an OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), and Core has an active backend PR for OTLP ingestion, bounded stores, REST endpoints, SignalR live updates, collector configuration, and tests in [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537). The remaining product work is to merge, release, document, and correlate this with workflow incidents.
|
||||
- Add default workflow semantic metrics: started, resumed, suspended, faulted, completed, active, activity executed/faulted, queue depth, recovery count, drain count, and dispatch latency. [#5988](https://github.com/elsa-workflows/elsa-core/issues/5988) remains the durable demand signal, while [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537) supplies the first current module boundary.
|
||||
- Build Studio diagnostics pages that are useful under pressure: live console, structured logs, OpenTelemetry traces/metrics/logs, workflow incident timelines, source health, dropped-event counters, source selection, filters, URL state, export/copy affordances, and direct deep links to workflow instances.
|
||||
- Make execution history easier to reason about: distinguish faulted, interrupted, cancelled, crash-recovered, retried, and operator-modified workflows consistently across API, Studio, logs, and metrics.
|
||||
- Connect diagnostics to workflow navigation: trace/span IDs, log source IDs, child workflow chains, alterations, runtime admin actions, and recovery actions should be correlated instead of presented as isolated tables.
|
||||
|
||||
|
|
@ -206,7 +211,7 @@ Recommended success measures:
|
|||
|
||||
High-value items:
|
||||
|
||||
- Publish canonical OIDC recipes for Blazor Server, WASM, separate server/studio, and all-in-one hosts. [#7181](https://github.com/elsa-workflows/elsa-core/issues/7181) shows both implementation and documentation demand.
|
||||
- Publish canonical OIDC recipes for Blazor Server, WASM, separate server/studio, all-in-one hosts, and reverse-proxy sub-path deployments. Studio `3.7.0` shipped the modern authentication modules, [#7181](https://github.com/elsa-workflows/elsa-core/issues/7181) shows Core-side implementation and documentation demand, and [elsa-studio#809](https://github.com/elsa-workflows/elsa-studio/pull/809) shows sub-path redirect URI handling is still being hardened.
|
||||
- Provide a production security guide: API keys, JWT/OIDC, default admin bootstrap, scripting trust levels, C# expression risks, Docker demo boundaries, secret masking, tenant isolation, and permission design.
|
||||
- Expand authorization coverage tests around workflow instances, runtime admin, diagnostics, labels, tenants, and HTTP endpoint activities.
|
||||
- Add Studio governance controls: tenant/role-based activity visibility, permission-aware menus/routes, feature-gated modules, and clear behavior for hidden activities in existing workflow definitions. [elsa-studio#584](https://github.com/elsa-workflows/elsa-studio/issues/584) captures the authoring side of this enterprise need.
|
||||
|
|
@ -226,10 +231,10 @@ Recommended success measures:
|
|||
|
||||
High-value items:
|
||||
|
||||
- Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367).
|
||||
- Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367), and [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) now provides an active Weaver AI Copilot foundation with AI abstractions, provider/session contracts, chat/tool endpoints, audit events, proposal persistence, EF Core storage, and integration/unit tests.
|
||||
- Provide an Elsa MCP/tooling surface for reading, validating, editing, and explaining workflow JSON/ElsaScript. This would make Elsa a strong fit for AI-enabled .NET development environments.
|
||||
- Align AI authoring with the Extensions Agents work: provider abstractions, MCP tools, OpenAI/Claude/local model support, tool approval, secrets handling, and Studio UX should share contracts instead of creating parallel AI stacks.
|
||||
- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, but it should be an accelerator on top of reliable authoring.
|
||||
- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, while [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) is still Core/backend-oriented and should not be treated as a complete Studio product surface.
|
||||
- Add "explain this workflow", "find risky activities", "suggest tests", and "generate migration notes" capabilities backed by workflow graph metadata.
|
||||
- Pair AI generation with validation: generated workflows should include test scaffolds, required input/output definitions, secrets handling, and clear review diffs.
|
||||
|
||||
|
|
@ -245,16 +250,17 @@ Near term:
|
|||
|
||||
1. Finish runtime confidence work: graceful shutdown remaining tasks, recovery clarity, distributed runtime regressions, security documentation, and OIDC recipes.
|
||||
2. Stabilize Studio authoring: designer regression harness, input/property-editor fixes, async dispatch/run UX, state machine Studio/docs completion, and a clear UI framework direction.
|
||||
3. Make workflow authoring easier to manage at scale: organization, search, progress/timeline APIs, testing docs, and user preference/table-state persistence.
|
||||
4. Reconcile shipped extension foundations with roadmap status: package manifests, Connections/Secrets, OpenAPI, Agents, schedulers, messaging, and integration maturity labels.
|
||||
3. Complete the diagnostics trilogy: merge/release the Core OpenTelemetry backend, connect it to the Studio OpenTelemetry page, document collector setup, and correlate traces/logs/metrics with workflow incidents.
|
||||
4. Make workflow authoring easier to manage at scale: organization, search, progress/timeline APIs, testing docs, and user preference/table-state persistence.
|
||||
5. Reconcile shipped extension foundations with roadmap status: package manifests, Connections/Secrets, OpenAPI, Agents, schedulers, messaging, and integration maturity labels.
|
||||
|
||||
Mid term:
|
||||
|
||||
1. OpenAPI activity provider plus connector SDK.
|
||||
2. Extension Platform: generated activities, contribution harnesses, Studio extension recipes, package manifest maturity, and marketplace/plugin installation path built on Nuplane and shell features.
|
||||
3. OpenTelemetry module boundary, default workflow metrics, and Studio trace/log/incident correlation.
|
||||
1. Promote [#7469](https://github.com/elsa-workflows/elsa-core/issues/7469) from deployment-platform PRD into an implementation spec covering manifests, immutable artifacts, dry-run validation, deployment history, and API/CLI surfaces.
|
||||
2. OpenAPI activity provider plus connector SDK.
|
||||
3. Extension Platform: generated activities, contribution harnesses, Studio extension recipes, package manifest maturity, and marketplace/plugin installation path built on Nuplane and shell features.
|
||||
4. Workflow debugging, replay-oriented incident analysis, child workflow navigation, and operator recovery UX.
|
||||
5. Agents provider matrix and MCP/tool lifecycle, with Studio UX that keeps generated workflows inspectable.
|
||||
5. Weaver AI Copilot productization and Agents provider matrix/MCP lifecycle, with Studio UX that keeps generated workflows inspectable.
|
||||
|
||||
Longer term:
|
||||
|
||||
|
|
|
|||
639
design/video/elsa-readme-video/package-lock.json
generated
639
design/video/elsa-readme-video/package-lock.json
generated
|
|
@ -8,10 +8,10 @@
|
|||
"name": "elsa-readme-video",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@remotion/cli": "4.0.462",
|
||||
"@remotion/cli": "4.0.469",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.14",
|
||||
|
|
@ -20,18 +20,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -660,23 +660,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/bundler": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.462.tgz",
|
||||
"integrity": "sha512-GlKjrFkrkl/UgiSTCzMPxRtTnOswOUozo5kk0TmK27tDlF9Mkm6Vyy/Dk5UptPIxDeTi8RIvGWrRRhgVUgzzeQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.469.tgz",
|
||||
"integrity": "sha512-vHJD/Ey3aTmjlFJ2m9stA+BK3uoacjNB7dT0hSSFQjuN0X4suNmSgA3lRhxPBcUFrrziy4HRXaiJp5H2y3Yc2Q==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"@remotion/media-parser": "4.0.462",
|
||||
"@remotion/studio": "4.0.462",
|
||||
"@remotion/studio-shared": "4.0.462",
|
||||
"@remotion/timeline-utils": "4.0.462",
|
||||
"@remotion/media-parser": "4.0.469",
|
||||
"@remotion/studio": "4.0.469",
|
||||
"@remotion/studio-shared": "4.0.469",
|
||||
"@remotion/timeline-utils": "4.0.469",
|
||||
"@rspack/core": "1.7.6",
|
||||
"@rspack/plugin-react-refresh": "1.6.1",
|
||||
"css-loader": "7.1.4",
|
||||
"esbuild": "0.28.0",
|
||||
"loader-utils": "2.0.4",
|
||||
"postcss": "8.5.10",
|
||||
"postcss-value-parser": "4.2.0",
|
||||
"react-refresh": "0.18.0",
|
||||
"remotion": "4.0.462",
|
||||
"remotion": "4.0.469",
|
||||
"style-loader": "4.0.0",
|
||||
"webpack": "5.105.0"
|
||||
},
|
||||
|
|
@ -686,22 +684,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/cli": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.462.tgz",
|
||||
"integrity": "sha512-ZFeXBM2bl1Eku8OroJyCOK+kflMTbJ7HBhUaH8qN3WSyX0dOngBKkNa9DRFbiDJVDGgE0ziG7uiSuU/oB1HPqQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.469.tgz",
|
||||
"integrity": "sha512-dv3hpY0CsjzkwPVXQJcJQHikXLHzupFyJg835rp5zBuqF+i/HmhZM8VZdh4LZDEut3OHJD3vkEgAk35R18Q8iQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"@remotion/bundler": "4.0.462",
|
||||
"@remotion/media-utils": "4.0.462",
|
||||
"@remotion/player": "4.0.462",
|
||||
"@remotion/renderer": "4.0.462",
|
||||
"@remotion/studio": "4.0.462",
|
||||
"@remotion/studio-server": "4.0.462",
|
||||
"@remotion/studio-shared": "4.0.462",
|
||||
"@remotion/bundler": "4.0.469",
|
||||
"@remotion/media-utils": "4.0.469",
|
||||
"@remotion/player": "4.0.469",
|
||||
"@remotion/renderer": "4.0.469",
|
||||
"@remotion/studio": "4.0.469",
|
||||
"@remotion/studio-server": "4.0.469",
|
||||
"@remotion/studio-shared": "4.0.469",
|
||||
"dotenv": "17.3.1",
|
||||
"minimist": "1.2.6",
|
||||
"prompts": "2.4.2",
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"bin": {
|
||||
"remotion": "remotion-cli.js",
|
||||
|
|
@ -714,9 +712,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/compositor-darwin-arm64": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.462.tgz",
|
||||
"integrity": "sha512-soKDNA0jSppazP9jxbz4OFQRTVS9mIj5w+5EtmcH23gCdnZejjCEO8839YE7nhHi5C7wgqU/sdkwRs3FyfP+tg==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.469.tgz",
|
||||
"integrity": "sha512-Ke/PljsHgvHIJD7rGlgTGkvmY044abjTUo2QI3Qq368NGzzEHe2USVyqycacKTilHCtcZG1n8CMbofpkXHurcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -726,9 +724,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-darwin-x64": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.462.tgz",
|
||||
"integrity": "sha512-xAkk7095Ud/Ugs3SP7GERnTwm2sljuZ4K89Gb5Y8tvdObqH70u1TMobzNWPcBOJcYOl7hgfc4dqV9pvMTHo+hw==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.469.tgz",
|
||||
"integrity": "sha512-e6usJHMZQN1qUrjLfaVXAHk212W+FMDzDQWJAnxzXDnb+diPpFvzmW8tGdZWTOjU5mBttEJQmS507zHvs2q4uQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -738,9 +736,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-linux-arm64-gnu": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.462.tgz",
|
||||
"integrity": "sha512-8Qy3lGKQsX/Kuxlji9AENnlpzRSDuG/K5DIe7o73YBitxey539sp1D3Ww0VCrE0uyH4Gj85KiPrT8YEESMcpCA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.469.tgz",
|
||||
"integrity": "sha512-/N7Bhq024Fw10n6kvx53D8EO3UQOOL6gmq8PJ3o7XORDSANzyYtTogyqHFMJ7LxZ5MLRP1vbdu2kqimBYJA3qw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -753,9 +751,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-linux-arm64-musl": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.462.tgz",
|
||||
"integrity": "sha512-dZjq3H8GngDdZs3/cqOdrPKOhW1UhQb99emYNx+G4Mv4kIpqX3sOEGYZNokCDZxR8hiAehI8BqblHk1NGDa0mQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.469.tgz",
|
||||
"integrity": "sha512-kVlqB1SSJ/ihVncgwEh9uKqMhjdtS+6NTuUTpZbVlEx34s8SN7Y7MV0cHHQnx4KiksbFvVABGVjL9a6yiqIuEw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -768,9 +766,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-linux-x64-gnu": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.462.tgz",
|
||||
"integrity": "sha512-nfqqBOc81+CnkmoOFhQF0EzqiGK1N5FwrcwrxexJxoQkxpNk4kMRp2CKyaOd6cHEx1hQKKCl/pPiScEez8ZwfA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.469.tgz",
|
||||
"integrity": "sha512-lP+Hzujpk3IB41thQEAf3vnK3bhfqjPrxytKqQ4gWxV830mhQ7g3Htgu9oWGYfshdbfWeMjlRF8rEClzcU/xBw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -783,9 +781,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-linux-x64-musl": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.462.tgz",
|
||||
"integrity": "sha512-AFNmG/Ll2lP3Hh9E7qjWkTIKaNfxZwQJeke2bWmcOV+tlWJa/mFccNVHnoDP5NpjpYwAHvemCUxmPIV1l2/7lw==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.469.tgz",
|
||||
"integrity": "sha512-5NV/W1rmZIQFn4ivYCNyg/YooUxKkd+EX/d+rdt/sidPzQUpySRm81F+pnnuqnVldlHmr6LGl9sNe5DSH9dJtw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -798,9 +796,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/compositor-win32-x64-msvc": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.462.tgz",
|
||||
"integrity": "sha512-lXjrweVS3cEpfR33TH7so93GKUtx77zfK0iKBkrTcwZw7IIYYIBG8Y7H2M2MTIW3xQDPtuJPhOuVTiUFcocmcw==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.469.tgz",
|
||||
"integrity": "sha512-0lEP9HvbCgQa7HxMl804DrE49kmctidNgSNHavVT7O7B4E03uxAt4kaRRy1P7SLeGMJ/1wKIYLfWtvV4sd7HKA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -810,25 +808,25 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@remotion/licensing": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.462.tgz",
|
||||
"integrity": "sha512-NbTiGVmqp7NJsLbjJP84jB5oLO92a6TJiq2KckpdpZlekLPbITpKp984rXMZjZgE/B0LkeYkIHRNO75AikFQLQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.469.tgz",
|
||||
"integrity": "sha512-ZSM0no+STTcEQAYGyyarXKkpQet18kXgJR59oTmRa0iqS+nN78OMBE4Uj7totpfmhXYV98T4gWgkGHqjDjqcBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@remotion/media-parser": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.462.tgz",
|
||||
"integrity": "sha512-wSXZcVhNppSxsw+kDF6Y00YqnTQvw88OU3sM6P2JR6IKzsKq2bSMgm4amqVs1K1hP7m3CAdx4NqG/5H9a9/kzw==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.469.tgz",
|
||||
"integrity": "sha512-mBHh2e7YK3CFiDvfXVoFxgxZrg7PtlDnYKCQHmkoqleBP3rpJlYaXeCgHt+mDwSUd0HFxGFdKYlDdXEn0Kh4kQ==",
|
||||
"license": "Remotion License https://remotion.dev/license"
|
||||
},
|
||||
"node_modules/@remotion/media-utils": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.462.tgz",
|
||||
"integrity": "sha512-naC7SVMO979E6kRzXxL3H1IoaNdTucE+7NbYDbt6ixrlsFtenpiRaVH0RlJPmYXmcwss3dvmmabVwrQ7RrMB5Q==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.469.tgz",
|
||||
"integrity": "sha512-L1Qk0PR0BQRhuCgBBnPSKi0zVTe2U2vx8Z2GmNgTqaJvNnzyMJ4WyWyMWYegNs68nFKoUJ5yskkI6oPUwrhCmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mediabunny": "1.45.0",
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
|
|
@ -836,12 +834,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/player": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.462.tgz",
|
||||
"integrity": "sha512-w/zBCWwLXsoNwsokaPwKxpdv+CBw52FuR3pQZegZvcnKwx1d8bq2P4xBjhCs+o5XP7kiRS0FQIIFYiCmXZXYgw==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.469.tgz",
|
||||
"integrity": "sha512-cAnin3JPrJx0mBFT7bcZ9N8xOwXygMIWeM4qHgDONfIwgqouir8gQFPIxRtyAzNubMl8xvLjbPSBbDeusNDAGw==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
|
|
@ -849,27 +847,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/renderer": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.462.tgz",
|
||||
"integrity": "sha512-qwOsGvtGb3CIpYMrVeO+wTwJN+C3uigT9gKyM0tl24IrPbq8UWqkX1mDJv5pYcWUbCj1mEdeakh7ue9+zgdEgA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.469.tgz",
|
||||
"integrity": "sha512-9529YcfPbZCQCVhq/eGQaqsuQzAeXClWzq9Eq3vC1UpJrAwsA3DfpDZiwu8IOXVhKqR4uouYJZQagcAahhH9bg==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"@remotion/licensing": "4.0.462",
|
||||
"@remotion/streaming": "4.0.462",
|
||||
"@remotion/licensing": "4.0.469",
|
||||
"@remotion/streaming": "4.0.469",
|
||||
"execa": "5.1.1",
|
||||
"extract-zip": "2.0.1",
|
||||
"remotion": "4.0.462",
|
||||
"remotion": "4.0.469",
|
||||
"source-map": "0.8.0-beta.0",
|
||||
"ws": "8.17.1"
|
||||
"ws": "8.20.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@remotion/compositor-darwin-arm64": "4.0.462",
|
||||
"@remotion/compositor-darwin-x64": "4.0.462",
|
||||
"@remotion/compositor-linux-arm64-gnu": "4.0.462",
|
||||
"@remotion/compositor-linux-arm64-musl": "4.0.462",
|
||||
"@remotion/compositor-linux-x64-gnu": "4.0.462",
|
||||
"@remotion/compositor-linux-x64-musl": "4.0.462",
|
||||
"@remotion/compositor-win32-x64-msvc": "4.0.462"
|
||||
"@remotion/compositor-darwin-arm64": "4.0.469",
|
||||
"@remotion/compositor-darwin-x64": "4.0.469",
|
||||
"@remotion/compositor-linux-arm64-gnu": "4.0.469",
|
||||
"@remotion/compositor-linux-arm64-musl": "4.0.469",
|
||||
"@remotion/compositor-linux-x64-gnu": "4.0.469",
|
||||
"@remotion/compositor-linux-x64-musl": "4.0.469",
|
||||
"@remotion/compositor-win32-x64-msvc": "4.0.469"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
|
|
@ -877,29 +874,29 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/streaming": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.462.tgz",
|
||||
"integrity": "sha512-o3+GgxZURUr1wwlTSKg3JRIF/r3ctSMUrMFB3rjxlcXKNW4Z/Izkho2lWmZlONYtZHJVshg6hfS3OxjTK2zwdA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.469.tgz",
|
||||
"integrity": "sha512-ACYjvcoCxIhVLICgm6zNyxKtXOsRnQ+GocWyY8SdHS2LNpLj/ihRj2xXAdJcuqQI/FaITHIvMiK9kwGAv3J6iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@remotion/studio": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.462.tgz",
|
||||
"integrity": "sha512-Rl+VhBesr1cCDuFogE2p+oAIF/IVM0fQYZDWWSLHFwF37gb6jLIfEgrrFqgarPwNTAEZwY2qqr2c7J3uooeH1Q==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.469.tgz",
|
||||
"integrity": "sha512-YMwNokK3XDfhCjZs0J6zj4kcOXlb96YugMMfOY3xBHyVmbKOnSyhNHyZ07E9Kr48p2lorzzS6n6kpFrpOt2FLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@remotion/media-utils": "4.0.462",
|
||||
"@remotion/player": "4.0.462",
|
||||
"@remotion/renderer": "4.0.462",
|
||||
"@remotion/studio-shared": "4.0.462",
|
||||
"@remotion/timeline-utils": "4.0.462",
|
||||
"@remotion/web-renderer": "4.0.462",
|
||||
"@remotion/zod-types": "4.0.462",
|
||||
"@remotion/media-utils": "4.0.469",
|
||||
"@remotion/player": "4.0.469",
|
||||
"@remotion/renderer": "4.0.469",
|
||||
"@remotion/studio-shared": "4.0.469",
|
||||
"@remotion/timeline-utils": "4.0.469",
|
||||
"@remotion/web-renderer": "4.0.469",
|
||||
"@remotion/zod-types": "4.0.469",
|
||||
"mediabunny": "1.45.0",
|
||||
"memfs": "3.4.3",
|
||||
"open": "8.4.2",
|
||||
"remotion": "4.0.462",
|
||||
"remotion": "4.0.469",
|
||||
"semver": "7.5.3",
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
|
|
@ -909,54 +906,54 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/studio-server": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.462.tgz",
|
||||
"integrity": "sha512-RI4KwdLju6yKO1IDDAtLVy0gelYf+rfLqhpRpRoutFj3X9euajbX2o7Fghfc3Qn6UfAz3VA78U8z4ZO75BAeQg==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.469.tgz",
|
||||
"integrity": "sha512-QnkXtHS6exjHXEQbIK8qXN/8c6jAO6FKrK7917+8E/+0Bl8I18K63yX/CxtONcRuj7qOPfq/s0fIpBO2awczbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "7.24.1",
|
||||
"@babel/types": "7.24.0",
|
||||
"@remotion/bundler": "4.0.462",
|
||||
"@remotion/renderer": "4.0.462",
|
||||
"@remotion/studio-shared": "4.0.462",
|
||||
"@remotion/bundler": "4.0.469",
|
||||
"@remotion/renderer": "4.0.469",
|
||||
"@remotion/studio-shared": "4.0.469",
|
||||
"memfs": "3.4.3",
|
||||
"open": "8.4.2",
|
||||
"prettier": "3.8.1",
|
||||
"recast": "0.23.11",
|
||||
"remotion": "4.0.462",
|
||||
"remotion": "4.0.469",
|
||||
"semver": "7.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@remotion/studio-shared": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.462.tgz",
|
||||
"integrity": "sha512-j8Q5+E4IWqpKWwSanxGI7+WG1rblpyf9Fb65cUoXmGrC28gq+dMqvPo+od2gSd77hfKZ80Po1u45cdP83B9tNA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.469.tgz",
|
||||
"integrity": "sha512-JN4jdfaLNYLo/aV5jPt13A7dheRm3MuF27Y9ON0Gs/VZva9G+nHyz89zUqMYWLaXU3gG5Sq9HkFa/lGMWC+2Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
}
|
||||
},
|
||||
"node_modules/@remotion/timeline-utils": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.462.tgz",
|
||||
"integrity": "sha512-xC6ErhwO9Jz6oP+0vfJHJVxEl7ZVTbyJOEbz4BzT1h0Wq4f6d9hH7d8VvmrvZ2IQwb1VaqrkR7URvaW+VfJ14A==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.469.tgz",
|
||||
"integrity": "sha512-Jrl0IUdgljfh64SgOxkFvgMX4UY1nYxDp8Hddrk2R8edIcVUJrMoe2+xLlgFoKwyhyZNE4kHNXnrVlUoSI4d5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mediabunny": "1.45.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remotion/web-renderer": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.462.tgz",
|
||||
"integrity": "sha512-Wj2IIACwNaRWGxc79LKs1vzbIEzhvs63u1rnJwvh+EDuHbhMZBCU7wJGVuSKZifNsKb9k999HiSS43d+j1OhpQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.469.tgz",
|
||||
"integrity": "sha512-6Bgx3zLy4AgQiT2ERQCqiBUatNfQhZ4amod6kIy3Xmc8nUw7m2KtzUXZfy7V+Ln3/yo3cRcBa1ehFSRiNbUfCg==",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@mediabunny/aac-encoder": "1.45.0",
|
||||
"@mediabunny/flac-encoder": "1.45.0",
|
||||
"@mediabunny/mp3-encoder": "1.45.0",
|
||||
"@remotion/licensing": "4.0.462",
|
||||
"@remotion/licensing": "4.0.469",
|
||||
"mediabunny": "1.45.0",
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
|
|
@ -964,12 +961,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@remotion/zod-types": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.462.tgz",
|
||||
"integrity": "sha512-q+wgcvD4AVFg17hPvpklZUU1oUt/A/tK1dTNi1o7NyXZmqic3Ko0iZpW01lEe5AsOW7so4XSWgv89Gs+r/R4dQ==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.469.tgz",
|
||||
"integrity": "sha512-AfWk/Y4vRepXAsQI41lHwjZyxm0KtNjx04EfTdfvKZq5lcTAEvLcpRlVQhGBazuCY4XXbhsNTSANeOpNAJF2yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "4.3.6"
|
||||
|
|
@ -1240,9 +1237,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
|
|
@ -1268,16 +1265,6 @@
|
|||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
|
|
@ -1518,9 +1505,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.29",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
||||
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
||||
"version": "2.10.33",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz",
|
||||
"integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
|
|
@ -1529,15 +1516,6 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
|
|
@ -1571,15 +1549,6 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -1587,9 +1556,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001792",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
|
||||
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
|
||||
"version": "1.0.30001793",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
||||
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -1635,6 +1604,65 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
|
||||
"integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"icss-utils": "^5.1.0",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss-modules-extract-imports": "^3.1.0",
|
||||
"postcss-modules-local-by-default": "^4.0.5",
|
||||
"postcss-modules-scope": "^3.2.0",
|
||||
"postcss-modules-values": "^4.0.0",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
|
||||
"webpack": "^5.27.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rspack/core": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/semver": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
|
|
@ -1642,23 +1670,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
|
|
@ -1681,33 +1692,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.357",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz",
|
||||
"integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==",
|
||||
"version": "1.5.364",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
|
||||
"integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.21.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz",
|
||||
"integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==",
|
||||
"version": "5.22.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz",
|
||||
"integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
|
|
@ -1870,41 +1863,6 @@
|
|||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -1927,15 +1885,6 @@
|
|||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
|
|
@ -2000,6 +1949,18 @@
|
|||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/icss-utils": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
||||
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
|
|
@ -2071,18 +2032,6 @@
|
|||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
|
@ -2105,20 +2054,6 @@
|
|||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
|
|
@ -2209,12 +2144,6 @@
|
|||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
|
|
@ -2240,10 +2169,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.44",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
|
||||
"integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
|
||||
"license": "MIT"
|
||||
"version": "2.0.46",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
|
||||
"integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
|
|
@ -2257,15 +2189,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
|
|
@ -2307,12 +2230,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -2320,9 +2237,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -2339,7 +2256,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
|
@ -2347,6 +2264,78 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules-extract-imports": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
|
||||
"integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules-local-by-default": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
|
||||
"integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"icss-utils": "^5.0.0",
|
||||
"postcss-selector-parser": "^7.0.0",
|
||||
"postcss-value-parser": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules-scope": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
|
||||
"integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules-values": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
|
||||
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"icss-utils": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
|
|
@ -2381,16 +2370,6 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -2456,9 +2435,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/remotion": {
|
||||
"version": "4.0.462",
|
||||
"resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.462.tgz",
|
||||
"integrity": "sha512-0bcwIiN7H0IcHtcDlfWHE2ho46RF0VmnVN1fAc26uMkonmAMMhqB+1aogfTE8eiYESEzFokh0lRc0t6pI8wzNA==",
|
||||
"version": "4.0.469",
|
||||
"resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.469.tgz",
|
||||
"integrity": "sha512-wD8LK3bFfcv44gnj8KLS1R1iGI+n0XGtPvdIiIaGfE+CTX3p2FJYKw2Vc5OfyfdVSACLn3r654XW9VteA9cTIQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
|
|
@ -2648,9 +2627,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.47.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz",
|
||||
"integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==",
|
||||
"version": "5.48.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
|
||||
"integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
|
|
@ -2666,9 +2645,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
|
||||
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz",
|
||||
"integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
|
|
@ -2805,6 +2784,12 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
|
||||
|
|
@ -2873,9 +2858,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz",
|
||||
"integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
|
||||
"integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
|
|
@ -2907,16 +2892,10 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
|
@ -2940,16 +2919,6 @@
|
|||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
"still": "remotion still ElsaReadme ../exports/elsa-workflows-readme-poster.png --frame=36"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remotion/cli": "4.0.462",
|
||||
"@remotion/cli": "4.0.469",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"remotion": "4.0.462"
|
||||
"remotion": "4.0.469"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.14",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 718 KiB After Width: | Height: | Size: 718 KiB |
438
doc/website/product-website-feature-source.md
Normal file
438
doc/website/product-website-feature-source.md
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
# Elsa Product Website Feature Source
|
||||
|
||||
This document is source material for a product website aimed at developers, architects, and CTOs evaluating workflow engines. It was compiled from:
|
||||
|
||||
- `elsa-core`: `/Users/sipke/Projects/Elsa/elsa-core`
|
||||
- `elsa-studio`: `/Users/sipke/Projects/Elsa/elsa-studio`
|
||||
- `elsa-extensions`: `/Users/sipke/Projects/Elsa/elsa-extensions-investigation` (`Elsa.Extensions.sln`)
|
||||
|
||||
Use this as the comprehensive feature inventory. The website should not show every item with equal weight; the final section contains instructions for Lovable AI to select and present the strongest public-facing set.
|
||||
|
||||
## Core Workflow Engine
|
||||
|
||||
### Embeddable .NET Workflow Engine
|
||||
|
||||
Elsa runs inside any .NET application and gives teams a workflow runtime they can embed directly into their own products, services, portals, back-office systems, and integration platforms. Workflows can be hosted in ASP.NET Core, run from worker services, or exposed through Elsa's server-style API surface. This matters to engineering teams that need workflow automation without handing orchestration to a black-box SaaS product.
|
||||
|
||||
### Code-First, Designer-First, and JSON Workflows
|
||||
|
||||
Workflows can be authored in C#, visually in Elsa Studio, or represented as JSON. Developers can keep critical workflows close to source control, product teams can iterate visually, and platforms can import/export workflow definitions through APIs or storage providers. This gives organizations a practical path from developer-owned automation to collaborative workflow operations.
|
||||
|
||||
### Long-Running and Short-Running Workflows
|
||||
|
||||
Elsa supports short-running automations as well as durable, long-running business processes that wait for external events, approvals, timers, or callbacks. The runtime persists workflow state, bookmarks, triggers, execution records, and incidents, allowing processes to pause and resume across application restarts and infrastructure events.
|
||||
|
||||
### Rich Control Flow
|
||||
|
||||
Elsa includes core control-flow activities such as `Sequence`, `If`, `Switch`, `For`, `ForEach`, `While`, `Parallel`, `Fork`, `Break`, `End`, `Finish`, `Complete`, and `Fault`. Teams can model simple linear automations, branching business rules, parallel work, loops, and failure paths without building custom orchestration logic for every process.
|
||||
|
||||
### Flowcharts, Sequences, and State Machines
|
||||
|
||||
Elsa's engine includes flowchart, sequence, and state machine execution models. Flowcharts support visual business-process modeling with joins, forks, switches, and token-based execution semantics. Sequences provide straightforward procedural automation. State machines model named states and trigger-driven transitions for processes that are naturally lifecycle-oriented.
|
||||
|
||||
### Triggers, Bookmarks, and Event-Driven Resumption
|
||||
|
||||
Workflows can start from triggers and resume from bookmarks. This is the foundation for event-driven automation: HTTP requests, timers, messages, webhooks, user actions, or external systems can start or continue the right workflow instance. Bookmarks are indexed by the runtime so waiting workflows can be found and resumed efficiently.
|
||||
|
||||
### Background Dispatch and Runtime Workers
|
||||
|
||||
The workflow runtime includes background dispatch for workflows, stimuli, tasks, and activities. This allows requests to hand off work and return while Elsa continues processing asynchronously. It also supports patterns where one workflow dispatches another workflow without coupling the caller to immediate execution.
|
||||
|
||||
### Transactional Dispatch Outbox
|
||||
|
||||
Elsa can use a transactional outbox for workflow dispatches created from inside a running workflow. The outbox prevents silent loss when a process crashes between persisting workflow state and enqueueing a dispatch command. Delivery is at-least-once, with idempotency support for child workflow dispatches.
|
||||
|
||||
### Graceful Shutdown and Recovery
|
||||
|
||||
The runtime includes node-local quiescence, drain orchestration, ingress-source pause/resume behavior, and interrupted workflow recovery. This gives operators a controlled way to stop servers without accepting new work mid-drain, and it helps recover work that was interrupted by shutdowns or crashes.
|
||||
|
||||
### Runtime Administration APIs
|
||||
|
||||
Elsa exposes runtime administration endpoints for status, pause, resume, and force-drain operations. Operators can inspect and control workflow runtime state through API-driven automation rather than relying only on process-level controls.
|
||||
|
||||
### Dead-Lettered Bookmark Queue Management
|
||||
|
||||
Bookmark queue items that expire or exceed delivery attempts can be moved to a dead-letter store. APIs allow operators to inspect, replay, and delete dead-lettered items. This is valuable for production workflows where failed event delivery should be visible and recoverable.
|
||||
|
||||
### Workflow Versioning and Migration
|
||||
|
||||
Elsa supports workflow definition versioning and API-driven migration. Teams can evolve workflows over time, publish new versions, and manage compatibility between definitions and running instances.
|
||||
|
||||
### Workflow Management APIs
|
||||
|
||||
Elsa has API surfaces for definitions, instances, execution journals, activity executions, runtime administration, triggers, bookmarks, and related workflow management concerns. This allows workflow capabilities to be embedded into custom platforms and integrated with existing developer or operator tooling.
|
||||
|
||||
### Custom Activities
|
||||
|
||||
Developers can add first-party or product-specific activities with strongly typed inputs and outputs, descriptors, UI hints, and designer integration. This is the extension point that turns Elsa from a generic workflow engine into a domain automation platform for a specific business.
|
||||
|
||||
### Activities as Composable Building Blocks
|
||||
|
||||
Elsa models workflow work as activities. Activities can be composed into higher-level workflows, and workflows themselves can be exposed as callable units. This supports reuse and standardization across teams.
|
||||
|
||||
### Dynamic Activity Handling
|
||||
|
||||
Elsa includes dynamic and missing-activity handling, allowing workflow definitions and designer/API surfaces to evolve while maintaining controlled behavior when activity types are not available.
|
||||
|
||||
### Expressions and Scripting
|
||||
|
||||
Elsa supports dynamic expressions through C#, JavaScript, Python, and Liquid expression modules. Activities can evaluate runtime data, inputs, variables, outputs, workflow context, and external values without forcing every transformation into compiled code.
|
||||
|
||||
### Workflow Inputs, Outputs, Variables, and Correlation
|
||||
|
||||
Elsa includes typed input/output models, variables, workflow input, activity outputs, correlation, and runtime state capture. This gives workflows structured data flow and allows external systems to correlate events with the right workflow instance.
|
||||
|
||||
### Persistence-Agnostic Architecture
|
||||
|
||||
Elsa is persistence-agnostic and includes persistence packages for EF Core providers such as SQLite, SQL Server, PostgreSQL, MySQL, and Oracle. Extension packages also add Dapper and MongoDB persistence options. This lets teams choose infrastructure that fits existing operational standards.
|
||||
|
||||
### Multi-Tenancy and Identity
|
||||
|
||||
Elsa includes identity, authorization, tenant support, and tenant-aware runtime behavior. This is important for SaaS platforms and internal platforms where different teams, customers, or environments need isolated workflow definitions, instances, and permissions.
|
||||
|
||||
### Secrets Management
|
||||
|
||||
Elsa includes a secrets module design with in-memory, configuration-backed, and encrypted Elsa-managed stores. Secrets are intended for use by activities and integrations that need credentials without exposing sensitive values in workflow definitions.
|
||||
|
||||
### Labels, Key Values, SAS Tokens, and Supporting Infrastructure
|
||||
|
||||
Elsa includes supporting modules such as labels, key-value storage, SAS tokens, caching, mediator infrastructure, and feature/module composition. These are less visible as standalone marketing features but help developers build complete workflow-enabled products.
|
||||
|
||||
## Operations and Observability
|
||||
|
||||
### Workflow Execution Journal
|
||||
|
||||
Elsa stores workflow and activity execution records that can be exposed through APIs and Studio. Operators and developers can inspect what ran, what failed, and how a workflow reached its current state.
|
||||
|
||||
### Structured Log Capture
|
||||
|
||||
`Elsa.Diagnostics.StructuredLogs` captures semantic `ILogger` events from Elsa hosts, redacts sensitive data, stores recent records, exposes REST query endpoints, and streams live events to Studio over SignalR. Records include rendered messages, templates, properties, scopes, exceptions, source metadata, trace/span IDs, workflow context, tenant, and correlation fields.
|
||||
|
||||
### Durable Structured Log Persistence
|
||||
|
||||
Structured logs default to a bounded in-memory store, with relational/SQLite persistence available for durable storage. The design includes bounded write queues, batch writes, retention services, storage diagnostics, and dropped-write reporting.
|
||||
|
||||
### Console Log Streaming
|
||||
|
||||
`Elsa.Diagnostics.ConsoleLogs` captures raw stdout and stderr, preserves the original console output destination, redacts content before buffering or streaming, and exposes recent/live console lines through REST and SignalR. This helps operators diagnose container or process-level output without leaving the workflow management experience.
|
||||
|
||||
### OpenTelemetry Workflow Diagnostics
|
||||
|
||||
The extensions repository includes `Elsa.OpenTelemetry`, and Studio includes an OpenTelemetry diagnostics module. The intent is workflow-aware trace, metric, and log exploration where Core remains responsible for ingestion, redaction, storage, permissions, and API contracts while Studio presents normalized diagnostics views.
|
||||
|
||||
### Configurable Workflow Logging Framework
|
||||
|
||||
The extensions repository includes a logging framework with configurable sinks such as console and Serilog, appsettings-based configuration, custom sink factories, and a `Log` activity for emitting structured log entries from workflows.
|
||||
|
||||
### Source and Storage Diagnostics
|
||||
|
||||
Diagnostics modules track source metadata, source health, storage pressure, dropped durable writes, dropped console lines, and live subscription state. These details are useful in clustered or high-volume environments where observability itself needs operational guardrails.
|
||||
|
||||
## Elsa Studio
|
||||
|
||||
### Modular Workflow Management UI
|
||||
|
||||
Elsa Studio is a modular Blazor application framework built with MudBlazor for managing workflows and related entities. It can run as Blazor Server, Blazor WebAssembly, hosted WebAssembly, or custom elements depending on host requirements.
|
||||
|
||||
### Visual Workflow Designer
|
||||
|
||||
Studio provides a drag-and-drop workflow designer for authoring workflows visually. It integrates with Elsa activity descriptors and UI hints so custom activities can appear in the designer alongside built-in activities.
|
||||
|
||||
### Workflow Definition and Instance Management
|
||||
|
||||
Studio includes workflow modules for managing definitions, viewing workflow state, and working with execution/runtime data exposed by the backend. This turns the engine from a library into an operator-facing workflow platform.
|
||||
|
||||
### Real-Time Workflow and Diagnostics Updates
|
||||
|
||||
Studio uses SignalR-backed clients for live updates such as structured logs and console logs. Authentication providers can configure SignalR connections consistently, allowing live operational views to work with JWT, Elsa Identity, or OpenID Connect setups.
|
||||
|
||||
### Structured Logs Viewer
|
||||
|
||||
Studio's structured log viewer loads recent semantic logs, subscribes to live updates, supports source selection, filtering, pause/reconnect/clear behaviors, and exposes detailed log inspection with properties, scopes, exceptions, trace/span fields, tenant, workflow, correlation, and raw JSON.
|
||||
|
||||
### Console Logs Viewer
|
||||
|
||||
Studio includes a diagnostics console page for raw stdout/stderr streaming. Navigation is feature-gated by backend capability and permission, with unavailable and unauthorized states handled in the UI.
|
||||
|
||||
### OpenTelemetry Diagnostics Viewer
|
||||
|
||||
Studio includes a diagnostics OpenTelemetry shell at `/diagnostics/opentelemetry`, with expected views for resource search, trace search/detail, metrics, OTLP logs, storage diagnostics, and collector configuration.
|
||||
|
||||
### Authentication Options
|
||||
|
||||
Studio supports Elsa Identity and OpenID Connect authentication modules across Blazor Server and WebAssembly hosting models. The architecture includes token providers, automatic refresh, backend API token acquisition, SignalR authentication configuration, and custom unauthorized UI rendering.
|
||||
|
||||
### Localization
|
||||
|
||||
Studio includes localization modules for Blazor Server and WebAssembly. Applications can register localization providers, configure supported cultures, and translate the Studio UI for international teams.
|
||||
|
||||
### Extensible Studio Modules
|
||||
|
||||
Studio modules include dashboard, workflows, designer, labels, environments, security, UI hints, diagnostics, localization, authentication, secrets, workflow contexts, and agents. The modular architecture lets product teams compose the Studio experience that fits their platform.
|
||||
|
||||
### Performance-Oriented Designer
|
||||
|
||||
Recent designer work includes batched activity size calculation, activity size caching, and conditional size updates to improve large workflow loading. This matters for teams building complex workflows with many activities.
|
||||
|
||||
### React Wrapper and Custom Embedding
|
||||
|
||||
Studio includes wrapper projects and host variants that support embedding Elsa workflow design experiences into different frontends, including React-oriented integration samples.
|
||||
|
||||
## Built-In Core Capabilities and Activities
|
||||
|
||||
### HTTP Workflows
|
||||
|
||||
Elsa includes HTTP activities for exposing endpoints, sending HTTP requests, downloading HTTP files, and writing HTTP responses or file responses. This makes it natural to build API-triggered workflows, webhook receivers, integration endpoints, and workflow-backed HTTP services.
|
||||
|
||||
### Scheduling and Timers
|
||||
|
||||
Scheduling activities include cron, delay, start-at, and timer behavior. Workflows can run on schedules, pause until a specific time, or wait for durations without custom background job code.
|
||||
|
||||
### Alterations
|
||||
|
||||
Elsa includes alteration modules for applying operational changes to workflow instances. Extensions add a MassTransit-backed alteration background runner for broker-backed, more resilient background processing.
|
||||
|
||||
### Resilience
|
||||
|
||||
Elsa includes resilience modules for workflow execution and activity behavior. These support more robust process automation in failure-prone distributed systems.
|
||||
|
||||
### Workflow Providers
|
||||
|
||||
Elsa includes workflow providers for loading definitions from blob storage and ElsaScript sources. This allows teams to source workflows from external storage rather than only from the management database.
|
||||
|
||||
### ElsaScript DSL
|
||||
|
||||
The ElsaScript DSL module provides a workflow definition scripting format, useful for teams that want text-based workflow authoring and source-controlled definitions.
|
||||
|
||||
## Extensions Ecosystem
|
||||
|
||||
### AI Agents and Multi-Agent Workflows
|
||||
|
||||
The extensions repository includes an Elsa Agents module built on Microsoft's Agent Framework/Semantic Kernel Agents. It supports configuration-based agents, code-first agents, multi-agent workflows, tool calling, persistent conversations/state, REST APIs, persistence packages, OpenAI and Azure OpenAI providers, workflow activities, and Studio UI.
|
||||
|
||||
### Connections Framework
|
||||
|
||||
The extensions repository includes connection modules for registering, describing, and applying connection information to activities. This is a foundation for reusable, centrally managed integration credentials and connection settings.
|
||||
|
||||
### SQL Automation
|
||||
|
||||
The SQL extension provides `SqlQuery`, `SqlCommand`, and `SqlSingleValue` activities, with providers for MySQL, PostgreSQL, SQLite, and SQL Server. It supports parameterization of workflow inputs, outputs, variables, activity context, execution context, workflow properties, POCOs, dictionaries, arrays, lists, and JSON objects, plus SQL syntax highlighting in Studio.
|
||||
|
||||
### CSV Processing
|
||||
|
||||
The CSV extension provides a `ReadCsv` activity that can read byte arrays, streams, strings, URLs, and uploaded files. It supports delimiters, header detection, dictionary output, and optional strongly typed mapping through CsvHelper.
|
||||
|
||||
### Email Automation
|
||||
|
||||
The extensions repository includes an email module with a `SendEmail` activity. This is a common workflow primitive for notifications, approvals, and operational alerts.
|
||||
|
||||
### Slack Automation
|
||||
|
||||
The Slack extension includes activities for channels, messages, reminders, reactions, stars, files, users, search, and event triggers. Workflows can create/update/delete messages, pin/unpin messages, upload files, manage reminders, list/search users and channels, and react to Slack events.
|
||||
|
||||
### GitHub DevOps Automation
|
||||
|
||||
The GitHub extension includes activities for issues, comments, labels, pull requests, repositories, users, organizations, milestones, releases, gists, branch search, GraphQL queries, and webhook-style triggers for issues, pull requests, and branches. Tokens should be stored securely through Elsa secrets.
|
||||
|
||||
### Service Bus and Messaging
|
||||
|
||||
Extensions include Azure Service Bus, Kafka, and MassTransit packages. Activities can send/publish/produce messages and react to received messages, enabling event-driven workflow automation across common messaging infrastructure.
|
||||
|
||||
### Distributed Runtime with Proto.Actor
|
||||
|
||||
The extensions repository includes actor-based runtime packages, including Proto.Actor-backed workflow runtime and actor modules. This extends Elsa's throughput and distributed execution story beyond the base runtime.
|
||||
|
||||
### Distributed Caching
|
||||
|
||||
Extensions include distributed caching modules over MassTransit and Proto.Actor. These support clustered scenarios where local in-memory assumptions are not enough.
|
||||
|
||||
### Scheduling Backends
|
||||
|
||||
Extensions include Hangfire and Quartz scheduling packages, with Quartz EF Core providers for MySQL, PostgreSQL, SQL Server, and SQLite. This lets teams use mature scheduling infrastructure behind Elsa's workflow scheduling concepts.
|
||||
|
||||
### Persistence Extensions
|
||||
|
||||
Extensions add persistence options such as Dapper, MongoDB, and Elasticsearch. These broaden deployment choices for organizations that standardize on specific storage technologies.
|
||||
|
||||
### File and Blob Storage
|
||||
|
||||
Extensions include Azure Storage and local file storage activities such as upload blob, save file, and open file. Workflows can move files through storage systems as part of business processes.
|
||||
|
||||
### HTTP OpenAPI and Webhooks
|
||||
|
||||
Extensions include OpenAPI and webhook modules for HTTP workflows. These help expose, document, and consume HTTP-driven workflow integration surfaces.
|
||||
|
||||
### Compression and I/O
|
||||
|
||||
Extensions include I/O and compression modules, including ZIP archive creation. These are useful in document, file transfer, and batch processing workflows.
|
||||
|
||||
### Telnyx Telephony Automation
|
||||
|
||||
The Telnyx extension includes call-control activities and webhook triggers such as answer call, hang up, dial, bridge calls, speak text, gather DTMF input, get call status, and react to call hangup or general webhook events.
|
||||
|
||||
### Orchard Core Integration
|
||||
|
||||
The extensions repository includes Orchard Core integration modules, useful for teams building CMS-backed or modular application platforms that also need workflow automation.
|
||||
|
||||
### Retention
|
||||
|
||||
The retention extension includes cleanup strategies and collectors for workflow-related records. This supports operational lifecycle management for workflow data.
|
||||
|
||||
### Workflow Contexts
|
||||
|
||||
Workflow context modules let activities set and use contextual parameters associated with workflow context providers. Studio modules add UI support for workflow context activity settings.
|
||||
|
||||
### Drop-Ins
|
||||
|
||||
Drop-in modules suggest a packaging and extension mechanism for pluggable capabilities. These are relevant to teams building modular workflow platforms.
|
||||
|
||||
### Planned and Roadmap Extensions
|
||||
|
||||
The extensions README lists planned or in-development integrations for Telegram, Discord, Microsoft Teams, Gmail, Outlook, Google Calendar, Microsoft Calendar, Google Drive, OneDrive, Azure Storage, Dropbox, Azure DevOps, GitLab, Jenkins, Datadog, cloud functions, CRM systems, payment providers, AI providers, video platforms, and industrial protocols such as OPC UA, Modbus, and MQTT Sparkplug. These should be presented carefully as ecosystem direction unless confirmed released.
|
||||
|
||||
## Architectural Selling Points
|
||||
|
||||
### Modular Feature System
|
||||
|
||||
Elsa uses explicit feature/module registration, dependency declarations, and extension methods. Teams can add only the capabilities they need, keep hosts lean, and build their own modules using the same conventions as first-party packages.
|
||||
|
||||
### Provider-Neutral Boundaries
|
||||
|
||||
Many Elsa capabilities are expressed behind provider contracts: persistence, structured logs, scheduling, workflow providers, SQL clients, authentication, storage, logging sinks, and diagnostics. This gives architects flexibility to standardize on their own infrastructure.
|
||||
|
||||
### API-First and Studio-Ready
|
||||
|
||||
Core capabilities are exposed through APIs and then surfaced in Studio. This means teams can automate through APIs, use Studio as an operator interface, or build their own UI on top of the same contracts.
|
||||
|
||||
### Production Operations Mindset
|
||||
|
||||
The runtime includes operational concepts such as graceful shutdown, recovery scanning, dead-letter queues, storage diagnostics, redaction, bounded buffers, authorization, tenant-aware behavior, and runtime admin endpoints. These are the details architects and CTOs look for when evaluating production fit.
|
||||
|
||||
### Developer Extensibility
|
||||
|
||||
Elsa is designed for developers to extend: custom activities, expression providers, API endpoints, stores, workflow providers, runtime ingress sources, Studio modules, authentication providers, log sinks, SQL clients, agent providers, and integration modules all have clear extension paths.
|
||||
|
||||
### Open Source and Ecosystem Friendly
|
||||
|
||||
Elsa is distributed through NuGet packages, Docker images, GitHub repositories, and community channels. The ecosystem includes Core, Studio, Extensions, Templates, package catalogs, and professional support options.
|
||||
|
||||
## Recommended Website Feature Selection
|
||||
|
||||
For the public product website, do not display all features as a flat checklist. Select the most compelling features for evaluators:
|
||||
|
||||
1. Visual workflow designer plus embeddable .NET runtime.
|
||||
2. Code-first, visual, and JSON authoring.
|
||||
3. Long-running durable workflows with triggers and bookmarks.
|
||||
4. Rich control flow, including flowcharts, sequences, state machines, and parallel execution.
|
||||
5. API-first workflow management and runtime administration.
|
||||
6. Persistence choices and production-ready storage boundaries.
|
||||
7. Observability: execution journal, structured logs, console logs, OpenTelemetry direction.
|
||||
8. Security and platform fit: identity, authorization, multi-tenancy, secrets.
|
||||
9. Extensibility: custom activities, modules, expression providers, Studio modules.
|
||||
10. Integration ecosystem: HTTP, scheduling, SQL, Slack, GitHub, messaging, files, AI agents.
|
||||
11. Operational readiness: graceful shutdown, recovery, dead-letter handling, durable dispatch outbox.
|
||||
12. Studio as a modular operations console, not only a designer.
|
||||
|
||||
## Suggested Public Website Copy
|
||||
|
||||
### Hero
|
||||
|
||||
**Title:** Workflow automation for .NET teams
|
||||
|
||||
**Description:** Elsa is an open-source workflow engine and visual workflow platform for building long-running, event-driven, and integration-heavy processes inside your own .NET applications.
|
||||
|
||||
### Feature: Build Workflows Your Way
|
||||
|
||||
**Title:** Code-first, visual, or JSON workflows
|
||||
|
||||
**Description:** Define workflows in C#, design them visually in Elsa Studio, or store them as JSON for API-driven platforms. Developers can keep critical automation close to source control while business and operations teams collaborate through a visual designer.
|
||||
|
||||
### Feature: Durable Event-Driven Runtime
|
||||
|
||||
**Title:** Long-running workflows that wait, resume, and recover
|
||||
|
||||
**Description:** Model processes that pause for timers, webhooks, approvals, messages, and external events. Elsa persists workflow state, indexes triggers and bookmarks, and resumes the right instance when the next signal arrives.
|
||||
|
||||
### Feature: Visual Studio Experience
|
||||
|
||||
**Title:** A modular Studio for designers and operators
|
||||
|
||||
**Description:** Elsa Studio provides a Blazor-based workflow designer, management UI, diagnostics views, authentication modules, localization, and extensible modules for custom platform experiences.
|
||||
|
||||
### Feature: Integration-Ready
|
||||
|
||||
**Title:** Connect workflows to real systems
|
||||
|
||||
**Description:** Use built-in and extension activities for HTTP, scheduling, SQL, CSV, email, Slack, GitHub, Azure Service Bus, Kafka, files, storage, telephony, and AI agents. Add custom activities when your domain needs first-class workflow building blocks.
|
||||
|
||||
### Feature: Production Operations
|
||||
|
||||
**Title:** Built for production workflow operations
|
||||
|
||||
**Description:** Elsa includes runtime administration, execution journals, graceful shutdown, recovery scanning, dead-letter handling, structured log capture, console streaming, redaction, permissions, and storage diagnostics.
|
||||
|
||||
### Feature: Architecture Fit
|
||||
|
||||
**Title:** Modular, provider-neutral, and extensible
|
||||
|
||||
**Description:** Compose only the features you need, plug in your persistence and infrastructure choices, and extend the platform with custom modules, activities, expression providers, APIs, authentication providers, and Studio screens.
|
||||
|
||||
### Feature: Observability
|
||||
|
||||
**Title:** See what workflows are doing
|
||||
|
||||
**Description:** Inspect workflow execution history, activity records, structured logs, raw console output, source metadata, trace and span correlation, workflow context, tenant, and correlation fields from API and Studio diagnostics views.
|
||||
|
||||
### Feature: Security and Multi-Tenancy
|
||||
|
||||
**Title:** Designed for secure platforms
|
||||
|
||||
**Description:** Elsa includes identity, authorization, tenant-aware behavior, secrets management, authenticated SignalR diagnostics, and permission-gated APIs for teams building SaaS, internal platforms, and regulated automation systems.
|
||||
|
||||
### Feature: AI Workflow Orchestration
|
||||
|
||||
**Title:** Orchestrate agents inside workflows
|
||||
|
||||
**Description:** Elsa Agents brings configuration-based and code-first AI agents, multi-agent workflows, tool calling, stateful conversations, OpenAI and Azure OpenAI providers, APIs, persistence, and Studio integration into the workflow model.
|
||||
|
||||
## Lovable AI Instructions
|
||||
|
||||
Use the source inventory above to design a product website section for Elsa Workflows. The target audience is developers, software architects, engineering managers, and CTOs evaluating workflow engines for .NET platforms.
|
||||
|
||||
Prioritize clarity, technical credibility, and enterprise-readiness over hype. Select the most interesting features rather than displaying the complete inventory. The site should make Elsa feel like a serious embeddable workflow platform with a strong visual designer, production runtime, and integration ecosystem.
|
||||
|
||||
Recommended structure:
|
||||
|
||||
1. Hero: "Workflow automation for .NET teams" with a concise description about open-source, embeddable, event-driven workflow automation.
|
||||
2. Primary feature grid: 6-8 cards covering authoring flexibility, durable runtime, visual Studio, integrations, operations, observability, security, and extensibility.
|
||||
3. Architecture band: explain modular/provider-neutral design, custom activities, APIs, and persistence choices.
|
||||
4. Integrations band: show categories rather than a huge checklist. Use "HTTP & APIs", "Scheduling", "Databases", "Messaging", "DevOps", "Communication", "Files & Storage", and "AI Agents".
|
||||
5. Operations band: highlight graceful shutdown, recovery, dead-letter queues, runtime admin, logs, and diagnostics.
|
||||
6. CTA: invite visitors to try the Docker image, explore docs, or view GitHub.
|
||||
|
||||
Use these exact card titles unless the layout requires shorter labels:
|
||||
|
||||
- Build Workflows Your Way
|
||||
- Durable Event-Driven Runtime
|
||||
- Visual Workflow Studio
|
||||
- Integration-Ready Automation
|
||||
- Production Workflow Operations
|
||||
- Observable by Design
|
||||
- Secure Multi-Tenant Platform
|
||||
- Extensible by Developers
|
||||
|
||||
Use concise card descriptions derived from the suggested public copy. Avoid listing unreleased/planned extensions as if they are shipped. If planned ecosystem items are mentioned, label them as roadmap or ecosystem direction.
|
||||
|
||||
Visual direction:
|
||||
|
||||
- Use product screenshots or workflow/designer imagery prominently.
|
||||
- Keep the design technical, polished, and readable.
|
||||
- Avoid vague abstract SaaS imagery.
|
||||
- Favor architecture diagrams, workflow nodes, log/detail panels, integration icons, and code snippets.
|
||||
- Make the first viewport communicate that Elsa is both a workflow engine and a visual workflow platform.
|
||||
|
||||
Tone:
|
||||
|
||||
- Direct, practical, developer-first.
|
||||
- Avoid exaggerated claims such as "effortless", "revolutionary", or "no-code magic".
|
||||
- Emphasize control, extensibility, operational readiness, and .NET-native integration.
|
||||
|
|
@ -6,8 +6,12 @@ To collect workflow traces, configure OpenTelemetry to listen to the `Elsa.Workf
|
|||
|
||||
```csharp
|
||||
services.AddOpenTelemetry()
|
||||
.WithTracing(builder => builder.AddSource("Elsa.Workflows"))
|
||||
.WithMetrics(builder => builder.AddMeter("Elsa.Workflows"));
|
||||
.WithTracing(builder => builder
|
||||
.AddSource("Elsa.Workflows")
|
||||
.AddOtlpExporter())
|
||||
.WithMetrics(builder => builder
|
||||
.AddMeter("Elsa.Workflows")
|
||||
.AddOtlpExporter());
|
||||
```
|
||||
|
||||
If you previously enabled workflow tracing through the `Elsa.OpenTelemetry` extension package, avoid enabling both the extension tracing middleware and the first-party workflow spans for the same host unless duplicate workflow and activity spans are acceptable. Both integrations publish to the `Elsa.Workflows` activity source so existing collectors can keep the same source configuration.
|
||||
|
|
@ -30,3 +34,53 @@ The `Elsa.Workflows` meter emits:
|
|||
- `elsa.activity.duration` in seconds
|
||||
|
||||
Metric tags use the same low-cardinality workflow and activity metadata as the spans where practical. The started counter omits execution status tags because it records the pre-execution boundary; completed and faulted counters include terminal workflow status tags.
|
||||
|
||||
## Elsa Diagnostics Collector
|
||||
|
||||
The `Elsa.Diagnostics.OpenTelemetry` module can collect OTLP HTTP/protobuf telemetry for local diagnostics and Studio visualization. The default ingestion routes are:
|
||||
|
||||
- `/elsa/otlp/v1/traces`
|
||||
- `/elsa/otlp/v1/metrics`
|
||||
- `/elsa/otlp/v1/logs`
|
||||
|
||||
Use the collector for development and focused troubleshooting. Production deployments should usually export to an external OpenTelemetry Collector or observability backend unless Elsa collector capacity, retention, and ingress security are deliberately configured for that environment.
|
||||
|
||||
### .NET Setup
|
||||
|
||||
Configure the .NET OpenTelemetry SDK to collect Elsa workflow instrumentation and export via OTLP:
|
||||
|
||||
```csharp
|
||||
services.AddOpenTelemetry()
|
||||
.WithTracing(builder => builder
|
||||
.AddSource("Elsa.Workflows")
|
||||
.AddOtlpExporter())
|
||||
.WithMetrics(builder => builder
|
||||
.AddMeter("Elsa.Workflows")
|
||||
.AddOtlpExporter());
|
||||
```
|
||||
|
||||
Then configure standard OTEL environment variables for the process:
|
||||
|
||||
```bash
|
||||
OTEL_SERVICE_NAME=elsa-server
|
||||
OTEL_RESOURCE_ATTRIBUTES=service.instance.id=local-dev
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5000/elsa/otlp/v1
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
OTEL_BSP_SCHEDULE_DELAY=1000
|
||||
OTEL_METRIC_EXPORT_INTERVAL=1000
|
||||
```
|
||||
|
||||
If the collector is exposed beyond loopback, also configure the required API key header. The header name is returned by the collector configuration API; do not expose the secret value through Studio or documentation.
|
||||
|
||||
### Polyglot Setup
|
||||
|
||||
Non-.NET services can use the same standard OTEL variables:
|
||||
|
||||
```bash
|
||||
OTEL_SERVICE_NAME=worker
|
||||
OTEL_RESOURCE_ATTRIBUTES=service.instance.id=worker-1
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5000/elsa/otlp/v1
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
```
|
||||
|
||||
Use each language SDK's OTLP exporter package. Studio identifies resources by `service.name` and `service.instance.id` when those attributes are present.
|
||||
|
|
|
|||
|
|
@ -148,6 +148,14 @@ Core serializers live under [Serialization](../../src/modules/Elsa.Workflows.Cor
|
|||
|
||||
Custom constructor and additional converter configurators are registered by `WorkflowsFeature`.
|
||||
|
||||
### Workflow JSON Type Identifiers
|
||||
|
||||
Workflow JSON type resolution uses the shared `ISerializationTypeRegistry` from `Elsa.Common.Serialization`, not expression type aliases. Register workflow-serializable payload types through `SerializationTypeOptions`; keep `ExpressionOptions` for expression/type metadata only.
|
||||
|
||||
New workflow JSON writes preferred aliases when a type is registered. Compatibility reads also accept explicitly registered legacy names, including selected CLR names from older persisted workflow JSON. Unknown CLR names are rejected rather than loaded dynamically. Polymorphic object reads also reject abstract, interface, open generic, and unsupported collection targets unless the resolver can map a known collection interface to a concrete collection type.
|
||||
|
||||
Public API payloads that expose workflow JSON type identifiers should emit values from `ISerializationTypeRegistry`. For example, incident strategy descriptors return the alias that workflow JSON accepts, while registered legacy CLR names remain readable during the compatibility window.
|
||||
|
||||
## When To Change This Layer
|
||||
|
||||
Change workflow core only when you are changing engine semantics, activity contracts, execution state, serialization, core activity behavior, or flowchart behavior. If the change is about persisted definitions, API DTOs, background dispatch, or a module-specific transport, start in management, API, runtime, or the extension module instead.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ COPY *.props ./
|
|||
RUN dotnet restore "./src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj"
|
||||
|
||||
# Build and publish (UseAppHost=false creates platform independent binaries).
|
||||
WORKDIR /source/src/bundles/Elsa.Server.Web
|
||||
WORKDIR /source/src/apps/Elsa.Server.Web
|
||||
RUN dotnet build "Elsa.Server.Web.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish "Elsa.Server.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net10.0
|
||||
|
||||
|
|
@ -27,20 +27,21 @@ RUN apt-get update \
|
|||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3-dev \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
unzip \
|
||||
wget \
|
||||
&& update-ca-certificates \
|
||||
&& ln -s "$(find /usr/lib -name 'libpython3*.so' -print -quit)" /usr/local/lib/libpython3.so \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
ENV PYTHONNET_PYDLL=/usr/local/lib/libpython3.so
|
||||
|
||||
# Set environment variables for OpenTelemetry Auto-Instrumentation
|
||||
ENV OTEL_DOTNET_AUTO_HOME=/otel \
|
||||
|
|
|
|||
|
|
@ -23,18 +23,19 @@ COPY --from=build /app/publish ./
|
|||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3-dev \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
&& update-ca-certificates \
|
||||
&& ln -s "$(find /usr/lib -name 'libpython3*.so' -print -quit)" /usr/local/lib/libpython3.so \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
ENV PYTHONNET_PYDLL=/usr/local/lib/libpython3.so
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ COPY ./NuGet.Config ./
|
|||
COPY *.props ./
|
||||
|
||||
# restore packages.
|
||||
RUN dotnet restore "./src/apps/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj"
|
||||
RUN dotnet restore "./src/apps/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj"
|
||||
RUN dotnet restore "./src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj"
|
||||
|
||||
# build and publish (UseAppHost=false creates platform independent binaries).
|
||||
WORKDIR /source/src/apps/Elsa.ServerAndStudio.Web
|
||||
RUN dotnet build "Elsa.ServerAndStudio.Web.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish "Elsa.ServerAndStudio.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net10.0
|
||||
WORKDIR /source/src/apps/Elsa.ModularServer.Web
|
||||
RUN dotnet build "Elsa.ModularServer.Web.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish "Elsa.ModularServer.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net10.0
|
||||
|
||||
# move binaries into smaller base image.
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
|
|
@ -24,20 +23,21 @@ COPY --from=build /app/publish ./
|
|||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3-dev \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
&& update-ca-certificates \
|
||||
&& ln -s "$(find /usr/lib -name 'libpython3*.so' -print -quit)" /usr/local/lib/libpython3.so \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
ENV PYTHONNET_PYDLL=/usr/local/lib/libpython3.so
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.ServerAndStudio.Web.dll"]
|
||||
CMD ["dotnet", "Elsa.ModularServer.Web.dll"]
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ COPY ./NuGet.Config ./
|
|||
COPY *.props ./
|
||||
|
||||
# restore packages.
|
||||
RUN dotnet restore "./src/apps/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj"
|
||||
RUN dotnet restore "./src/apps/Elsa.Studio.Web/Elsa.Studio.Web.csproj"
|
||||
RUN dotnet restore "./src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj"
|
||||
|
||||
# build and publish (UseAppHost=false creates platform independent binaries).
|
||||
WORKDIR /source/src/apps/Elsa.Studio.Web
|
||||
RUN dotnet build "Elsa.Studio.Web.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish "Elsa.Studio.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net10.0
|
||||
WORKDIR /source/src/apps/Elsa.ModularServer.Web
|
||||
RUN dotnet build "Elsa.ModularServer.Web.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish "Elsa.ModularServer.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net10.0
|
||||
|
||||
# move binaries into smaller base image.
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
|
|
@ -32,4 +31,4 @@ RUN chmod +x /entrypoint.sh
|
|||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.Studio.Web.dll"]
|
||||
CMD ["dotnet", "Elsa.ModularServer.Web.dll"]
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@ services:
|
|||
- ASPNETCORE_URLS=http://+:80
|
||||
- Logging__LogLevel__Default=Information
|
||||
|
||||
aspire-dashboard:
|
||||
image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
|
||||
container_name: aspire-dashboard
|
||||
ports:
|
||||
- "18888:18888" # Dashboard UI
|
||||
- "4317:18889" # OTLP/gRPC
|
||||
- "4318:18890" # OTLP/HTTP
|
||||
environment:
|
||||
ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS: "true"
|
||||
|
||||
volumes:
|
||||
sqlserver_data:
|
||||
postgres-data:
|
||||
|
|
|
|||
|
|
@ -54,40 +54,48 @@ CERTEXT
|
|||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
|
||||
echo "$CERT_WORKDIR"
|
||||
cleanup_local_ca_server() {
|
||||
if [ -n "${SERVER_PID:-}" ]; then
|
||||
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
SERVER_PID=
|
||||
fi
|
||||
|
||||
if [ -n "${CERT_WORKDIR:-}" ]; then
|
||||
rm -rf "$CERT_WORKDIR"
|
||||
CERT_WORKDIR=
|
||||
fi
|
||||
}
|
||||
|
||||
run_extra_ca_test() {
|
||||
local cert_dir
|
||||
cert_dir=$(start_local_ca_server)
|
||||
trap "kill ${SERVER_PID:-0} >/dev/null 2>&1 || true; rm -rf '$cert_dir'" EXIT
|
||||
start_local_ca_server
|
||||
trap cleanup_local_ca_server EXIT
|
||||
|
||||
echo "[CA] Validating EXTRA_CA_CERT flow against local CA" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-v "${CERT_WORKDIR}:/certs:ro" \
|
||||
-e EXTRA_CA_CERT=/certs/ca.crt
|
||||
|
||||
echo "[CA] Validating SSL_CERT_FILE fallback" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-v "${CERT_WORKDIR}:/certs:ro" \
|
||||
-e SSL_CERT_FILE=/certs/ca.crt
|
||||
|
||||
mkdir -p "$cert_dir/dir"
|
||||
cp "$cert_dir/ca.crt" "$cert_dir/dir/custom-ca.crt"
|
||||
openssl rehash "$cert_dir/dir" >/dev/null 2>&1
|
||||
mkdir -p "$CERT_WORKDIR/dir"
|
||||
cp "$CERT_WORKDIR/ca.crt" "$CERT_WORKDIR/dir/custom-ca.crt"
|
||||
openssl rehash "$CERT_WORKDIR/dir" >/dev/null 2>&1
|
||||
|
||||
echo "[CA] Validating SSL_CERT_DIR fallback" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-v "${CERT_WORKDIR}:/certs:ro" \
|
||||
-e SSL_CERT_DIR=/certs/dir
|
||||
|
||||
kill "${SERVER_PID:-0}" >/dev/null 2>&1 || true
|
||||
wait "${SERVER_PID:-0}" 2>/dev/null || true
|
||||
rm -rf "$cert_dir"
|
||||
cleanup_local_ca_server
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
|
|
|
|||
31
specs/008-diagnostics-otel/checklists/requirements.md
Normal file
31
specs/008-diagnostics-otel/checklists/requirements.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Specification Quality Checklist: Diagnostics OpenTelemetry
|
||||
|
||||
**Purpose**: Validate Core specification completeness before implementation
|
||||
**Created**: 2026-05-25
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details in product requirements beyond Core system boundaries
|
||||
- [x] Focused on user value and backend diagnostics needs
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] Functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary Core flows
|
||||
- [x] Technical plan identifies Core module boundaries
|
||||
|
||||
## Notes
|
||||
|
||||
- Studio UI work is intentionally tracked in `elsa-studio/specs/008-diagnostics-otel`.
|
||||
50
specs/008-diagnostics-otel/contracts/otlp-ingest.md
Normal file
50
specs/008-diagnostics-otel/contracts/otlp-ingest.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Contract: OTLP Ingestion
|
||||
|
||||
## HTTP/protobuf
|
||||
|
||||
Default base path:
|
||||
|
||||
```text
|
||||
/elsa/otlp/v1
|
||||
```
|
||||
|
||||
Supported paths:
|
||||
|
||||
```text
|
||||
POST /elsa/otlp/v1/traces
|
||||
POST /elsa/otlp/v1/metrics
|
||||
POST /elsa/otlp/v1/logs
|
||||
```
|
||||
|
||||
Requests use OTLP protobuf bodies. Accepted payloads return `200 OK`; unauthorized requests return `401 Unauthorized`; malformed protobuf payloads return `400 Bad Request`; payloads over the configured request body limit return `413 Payload Too Large`. The current collector does not emit OTLP partial-success response bodies.
|
||||
|
||||
## gRPC
|
||||
|
||||
When enabled, Core exposes OTLP gRPC services:
|
||||
|
||||
```text
|
||||
opentelemetry.proto.collector.trace.v1.TraceService/Export
|
||||
opentelemetry.proto.collector.metrics.v1.MetricsService/Export
|
||||
opentelemetry.proto.collector.logs.v1.LogsService/Export
|
||||
```
|
||||
|
||||
When gRPC is disabled, collector configuration must return `GrpcEnabled = false` and no endpoint.
|
||||
|
||||
## Security
|
||||
|
||||
- Loopback-only development ingestion may run without an API key only when the request remote address is loopback and the advertised collector binding is loopback.
|
||||
- Non-loopback requests and non-loopback collector bindings must require a configured header such as `x-otlp-api-key`.
|
||||
- Secret header values are configured server-side, compared without exposing the configured value, and omitted from collector configuration responses.
|
||||
- Accepted telemetry is redacted before storage and live streaming.
|
||||
- OTLP sender auth is separate from diagnostics API user auth.
|
||||
|
||||
## Internal Contract
|
||||
|
||||
```csharp
|
||||
public interface IOpenTelemetryIngestor
|
||||
{
|
||||
ValueTask IngestAsync(OpenTelemetryBatch batch, CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
Transport-specific code parses payloads; shared ingestion normalizes, redacts, stores, and publishes live updates.
|
||||
48
specs/008-diagnostics-otel/contracts/provider-contract.md
Normal file
48
specs/008-diagnostics-otel/contracts/provider-contract.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Contract: Provider and Store
|
||||
|
||||
## Provider Facade
|
||||
|
||||
```csharp
|
||||
public interface IOpenTelemetryProvider
|
||||
{
|
||||
ValueTask<SearchTelemetryResourcesResult> SearchResourcesAsync(TelemetryResourceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<SearchTelemetryTracesResult> SearchTracesAsync(TelemetryTraceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<TelemetryTraceDetail?> GetTraceAsync(string traceId, CancellationToken cancellationToken = default);
|
||||
ValueTask<SearchTelemetryMetricsResult> SearchMetricsAsync(TelemetryMetricFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<SearchOtlpLogsResult> SearchLogsAsync(OtlpLogFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryStorageDiagnostics> GetStorageDiagnosticsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
## Store
|
||||
|
||||
```csharp
|
||||
public interface IOpenTelemetryStore
|
||||
{
|
||||
ValueTask WriteTracesAsync(IReadOnlyCollection<TelemetrySpan> spans, CancellationToken cancellationToken = default);
|
||||
ValueTask WriteMetricsAsync(IReadOnlyCollection<MetricInstrumentUpdate> metrics, CancellationToken cancellationToken = default);
|
||||
ValueTask WriteLogsAsync(IReadOnlyCollection<OtlpLogRecord> logs, CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
The concrete in-memory implementation also supports provider queries. A later durable provider may split write/query responsibilities if needed.
|
||||
|
||||
Search methods return bounded results using server-capped limits. Search ordering is newest receive time first. Trace detail ordering is parent/child chronological so Studio can render a waterfall without reassembling hierarchy from arbitrary span order.
|
||||
|
||||
## Redaction
|
||||
|
||||
```csharp
|
||||
public interface IOpenTelemetryRedactor
|
||||
{
|
||||
TelemetryResource Redact(TelemetryResource resource);
|
||||
TelemetrySpan Redact(TelemetrySpan span);
|
||||
MetricPoint Redact(MetricPoint point);
|
||||
OtlpLogRecord Redact(OtlpLogRecord logRecord);
|
||||
}
|
||||
```
|
||||
|
||||
Redaction occurs before store writes and live publishing.
|
||||
|
||||
## Capacity
|
||||
|
||||
The default in-memory provider uses configurable per-signal limits with drop-oldest overflow. Defaults must be at least 500 resources, 2,000 traces, 10,000 spans, 20,000 metric points, 10,000 OTLP log records, and 1,000 queued live updates per subscriber.
|
||||
67
specs/008-diagnostics-otel/contracts/rest-api.md
Normal file
67
specs/008-diagnostics-otel/contracts/rest-api.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Contract: REST API
|
||||
|
||||
Base path:
|
||||
|
||||
```text
|
||||
/diagnostics/opentelemetry
|
||||
```
|
||||
|
||||
All endpoints require the OpenTelemetry diagnostics view permission.
|
||||
|
||||
Search endpoints accept a caller-provided limit capped by server options. Trace, metric, and log search results return the latest matching window in ascending chronological order. Resource search results are ordered by most recently seen first. Trace detail spans are returned in parent/child order with chronological ordering inside each sibling set.
|
||||
|
||||
## Resources
|
||||
|
||||
```text
|
||||
POST /diagnostics/opentelemetry/resources/search
|
||||
```
|
||||
|
||||
Filters by service name, status, and text.
|
||||
|
||||
## Trace Search
|
||||
|
||||
```text
|
||||
POST /diagnostics/opentelemetry/traces/search
|
||||
```
|
||||
|
||||
Filters by resource key, service name, trace ID, workflow instance ID, workflow definition ID, status, text, and time range.
|
||||
|
||||
## Trace Detail
|
||||
|
||||
```text
|
||||
GET /diagnostics/opentelemetry/traces/{traceId}
|
||||
```
|
||||
|
||||
Returns trace summary, ordered spans, related resources, and OTLP logs for the trace when available.
|
||||
|
||||
## Metrics
|
||||
|
||||
```text
|
||||
POST /diagnostics/opentelemetry/metrics/search
|
||||
```
|
||||
|
||||
Filters by resource, instrument name, attribute text, and time range.
|
||||
|
||||
## OTLP Logs
|
||||
|
||||
```text
|
||||
POST /diagnostics/opentelemetry/logs/search
|
||||
```
|
||||
|
||||
Filters by resource, trace ID, span ID, severity, text, and time range.
|
||||
|
||||
## Collector Configuration
|
||||
|
||||
```text
|
||||
GET /diagnostics/opentelemetry/collector-configuration
|
||||
```
|
||||
|
||||
Returns HTTP endpoint metadata, nullable/disabled gRPC metadata, required header names, loopback status, and recommended non-secret OTEL environment variables.
|
||||
|
||||
## Storage Diagnostics
|
||||
|
||||
```text
|
||||
GET /diagnostics/opentelemetry/storage
|
||||
```
|
||||
|
||||
Returns capacity, dropped counts, and current resource/trace/series counts.
|
||||
39
specs/008-diagnostics-otel/contracts/signalr-hub.md
Normal file
39
specs/008-diagnostics-otel/contracts/signalr-hub.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Contract: SignalR Hub
|
||||
|
||||
Hub path:
|
||||
|
||||
```text
|
||||
/elsa/hubs/diagnostics/opentelemetry
|
||||
```
|
||||
|
||||
The hub requires the OpenTelemetry diagnostics view permission.
|
||||
|
||||
## Client-to-Server Methods
|
||||
|
||||
```text
|
||||
SubscribeAsync(OpenTelemetryTraceFilter filter)
|
||||
```
|
||||
|
||||
Filters support resource ID, service name, trace ID, workflow instance ID, workflow definition ID, status, text, and time range. Metric clients subscribe through the trace-filter-compatible live stream and apply metric-specific filtering client-side.
|
||||
|
||||
## Server-to-Client Events
|
||||
|
||||
```text
|
||||
ReceiveAsync(OpenTelemetryStreamItem item)
|
||||
```
|
||||
|
||||
`OpenTelemetryStreamItem` variants:
|
||||
|
||||
- `Resource`
|
||||
- `Trace`
|
||||
- `MetricPoint`
|
||||
- `Log`
|
||||
- `DroppedItems`
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- Subscriptions are bounded by server-side per-connection subscriber capacity.
|
||||
- Filter changes are represented by starting a new subscription.
|
||||
- Disconnected clients release subscriber resources.
|
||||
- When a subscriber queue is full, live updates are dropped and reported through `DroppedItems` summaries.
|
||||
- Subscriber overflow must not disconnect the client unless the connection itself is unhealthy or unauthorized.
|
||||
104
specs/008-diagnostics-otel/data-model.md
Normal file
104
specs/008-diagnostics-otel/data-model.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Data Model: Diagnostics OpenTelemetry
|
||||
|
||||
## Telemetry Resource
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `ResourceKey` | Stable key derived from service name, instance ID, and resource attributes. |
|
||||
| `ServiceName` | From `service.name` or `OTEL_SERVICE_NAME`; fallback is `unknown_service`. |
|
||||
| `ServiceInstanceId` | From `service.instance.id` when present. |
|
||||
| `ServiceVersion` | From `service.version` when present. |
|
||||
| `DeploymentEnvironment` | From `deployment.environment` when present. |
|
||||
| `Attributes` | Redacted resource attributes. |
|
||||
| `FirstSeen`, `LastSeen` | Backend receive timestamps. |
|
||||
| `Status` | Active, stale, or disconnected. |
|
||||
|
||||
## Telemetry Trace
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `TraceId` | Required trace identifier. |
|
||||
| `RootSpanId` | Root span when known. |
|
||||
| `Name` | Root span name or synthesized trace label. |
|
||||
| `ResourceKeys` | Participating resources. |
|
||||
| `StartTime`, `EndTime`, `Duration` | Derived from spans. |
|
||||
| `Status` | Ok, error, unset, or mixed. |
|
||||
| `SpanCount`, `ErrorCount` | Derived counters. |
|
||||
| `WorkflowInstanceId`, `WorkflowDefinitionId` | Elsa span attributes when present. |
|
||||
| `LastReceivedAt` | Backend receive timestamp. |
|
||||
|
||||
## Telemetry Span
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `TraceId`, `SpanId`, `ParentSpanId` | Trace hierarchy. |
|
||||
| `ResourceKey` | Emitting resource. |
|
||||
| `Name`, `Kind` | OTEL span metadata. |
|
||||
| `StartTime`, `EndTime`, `Duration` | Timing. |
|
||||
| `StatusCode`, `StatusDescription` | OTEL status. |
|
||||
| `Attributes` | Redacted span attributes. |
|
||||
| `Events` | Redacted events. |
|
||||
| `Links` | Span links. |
|
||||
| `Workflow*`, `Activity*`, `TenantId`, `CorrelationId` | Existing `Elsa.Workflows` semantic attributes when present: `workflow.*`, `workflow.activity.*`, and `elsa.tenant.id`. |
|
||||
| `ReceivedAt` | Backend receive timestamp. |
|
||||
|
||||
## Metric Instrument and Point
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `Name`, `Description`, `Unit`, `Type` | Instrument metadata. |
|
||||
| `ResourceKey` | Emitting resource. |
|
||||
| `Series` | Bounded collection by attribute set. |
|
||||
| `Timestamp`, `StartTimestamp` | Point timestamps. |
|
||||
| `Attributes` | Redacted point attributes. |
|
||||
| `Value` | Sum/gauge value. |
|
||||
| `Count`, `Sum`, `Min`, `Max`, `Buckets` | Histogram fields when present. |
|
||||
| `Temporality` | Cumulative or delta. |
|
||||
| `DroppedPointCount` | Capacity drop count. |
|
||||
|
||||
## OTLP Log Record
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `Id` | Backend assigned ID. |
|
||||
| `Timestamp`, `ObservedTimestamp`, `ReceivedAt` | OTLP and backend timing. |
|
||||
| `ResourceKey` | Emitting resource. |
|
||||
| `SeverityText`, `SeverityNumber` | OTEL severity. |
|
||||
| `Body` | Redacted body. |
|
||||
| `TraceId`, `SpanId` | Correlation fields. |
|
||||
| `Attributes` | Redacted log attributes. |
|
||||
|
||||
## Collector Configuration
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `HttpEndpoint` | Base endpoint suitable for `OTEL_EXPORTER_OTLP_ENDPOINT` with HTTP/protobuf. |
|
||||
| `GrpcEndpoint` | Endpoint suitable for gRPC when enabled; null when unavailable. |
|
||||
| `GrpcEnabled` | Whether gRPC ingestion is enabled. |
|
||||
| `GrpcDisabledReason` | Optional non-secret explanation when disabled. |
|
||||
| `RequiredHeaders` | Required ingestion header names without secret values. |
|
||||
| `RecommendedEnvironment` | Copyable non-secret OTEL environment variable values. |
|
||||
| `IsLoopbackOnly` | Whether ingestion is restricted to loopback. |
|
||||
| `RequiresApiKey` | Whether configured ingestion protection is required for advertised endpoints. |
|
||||
|
||||
## Storage Diagnostics
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `DroppedSpanCount` | Spans dropped by store capacity. |
|
||||
| `DroppedMetricPointCount` | Metric points dropped by capacity. |
|
||||
| `DroppedLogRecordCount` | OTLP logs dropped by capacity. |
|
||||
| `DroppedLiveUpdateCount` | Live updates dropped by subscriber capacity. |
|
||||
| `ResourceCount`, `TraceCount`, `MetricSeriesCount` | Current counts. |
|
||||
|
||||
## Storage Capacity Policy
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `MaxResources` | Default at least 500. |
|
||||
| `MaxTraces` | Default at least 2,000. |
|
||||
| `MaxSpans` | Default at least 10,000. |
|
||||
| `MaxMetricPoints` | Default at least 20,000 across all metric series. |
|
||||
| `MaxLogRecords` | Default at least 10,000. |
|
||||
| `MaxLiveUpdatesPerSubscriber` | Default at least 1,000 queued updates per SignalR subscriber. |
|
||||
| `OverflowPolicy` | Drop oldest item in the signal-specific buffer and increment the matching dropped counter. |
|
||||
149
specs/008-diagnostics-otel/plan.md
Normal file
149
specs/008-diagnostics-otel/plan.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Implementation Plan: Diagnostics OpenTelemetry
|
||||
|
||||
**Branch**: `008-diagnostics-otel` | **Date**: 2026-05-25 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/008-diagnostics-otel/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Add an opt-in `Elsa.Diagnostics.OpenTelemetry` Core diagnostics module that receives OTLP telemetry, normalizes it into Elsa diagnostics read models, redacts sensitive values before provider boundaries, stores recent telemetry in bounded memory, exposes authenticated REST APIs and SignalR live updates for Studio, and publishes collector configuration metadata for standard OTEL senders.
|
||||
|
||||
The module is a collector/API backend, not producer-side workflow tracing middleware. Existing `Elsa.Workflows.Core` `ActivitySource` and `Meter` instrumentation remains the workflow telemetry producer. The historical `Elsa.OpenTelemetry` module from `elsa-extensions` is not ported in v1.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: C# latest, nullable reference types enabled, implicit usings enabled.
|
||||
**Primary Dependencies**: OpenTelemetry SDK/proto contracts, ASP.NET Core endpoint patterns, optional gRPC services, SignalR, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, Elsa shell feature infrastructure, existing Elsa identity/authorization patterns, current `Elsa.Workflows.Core` instrumentation.
|
||||
**Storage**: Bounded in-memory telemetry repository for resources, traces, spans, metric instruments/points, OTLP log records, live subscribers, and dropped-count diagnostics. Defaults are configurable, drop oldest per signal on overflow, and start with at least 500 resources, 2,000 traces, 10,000 spans, 20,000 metric points, 10,000 OTLP log records, and 1,000 queued live updates per subscriber. No durable database schema in v1.
|
||||
**Testing**: xUnit unit tests for normalization, redaction, resource identity, filtering, bounded storage, dropped counts, and collector configuration; integration tests for HTTP/protobuf OTLP ingestion, optional gRPC ingestion when enabled, permissions, SignalR, API contracts, and workflow export-to-collector timing.
|
||||
**Target Platform**: ASP.NET Core Elsa Server on supported repository target frameworks.
|
||||
**Project Type**: Modular .NET library inside the existing Elsa solution.
|
||||
**Performance Goals**: Local workflow telemetry appears through diagnostics APIs within 2 seconds; trace detail handles 100 spans; metrics handle 20 instruments and 1,000 recent points; backend buffers remain bounded under overload.
|
||||
**Constraints**: Do not merge with Structured Logs or Console Logs. Do not port historical producer middleware. Do not start workflow/activity spans in the diagnostics module or mutate `Activity.Current`. Do not add durable OTEL persistence, vendor exporters, Kubernetes/Docker APIs, or Studio UI in Core. Non-loopback OTLP ingestion requires explicit protection.
|
||||
**Scale/Scope**: One Core module, unit/integration tests, docs/wiki quickstart updates, API/hub contracts, and sample host wiring if needed for verification.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
Evaluated against `.specify/memory/constitution.md` v1.1.0:
|
||||
|
||||
| Principle | Verdict | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| I. Modular Architecture | PASS | Work is a focused module under `src/modules/Elsa.Diagnostics.OpenTelemetry` with contracts, services, endpoints, hub, options, permissions, and shell feature. |
|
||||
| II. Composition & Extensibility | PASS | Ingestion, redaction, store, live feed, source registry, and collector configuration are explicit contracts/services. |
|
||||
| III. Convention-Driven Design | PASS | Names follow Elsa module, endpoint, feature, permission, and test conventions with American English. |
|
||||
| IV. Async & Pipeline Execution | PASS | Ingestion, provider queries, endpoints, live subscriptions, and shutdown paths are async/cancellation-aware. |
|
||||
| V. Testing Discipline | PASS | Plan requires unit/integration coverage for ingestion, security, storage, API, SignalR, and workflow telemetry timing. |
|
||||
| VI. Trunk-Based Development | PASS | Core backend module can land independently from Studio after API contracts stabilize. |
|
||||
| VII. Simplicity, SRP, DRY & KISS | PASS | V1 uses bounded in-memory storage and excludes durable storage, vendor exporters, app launcher behavior, and producer middleware changes. |
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/008-diagnostics-otel/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ ├── rest-api.md
|
||||
│ ├── otlp-ingest.md
|
||||
│ ├── signalr-hub.md
|
||||
│ └── provider-contract.md
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
src/modules/
|
||||
└── Elsa.Diagnostics.OpenTelemetry/
|
||||
├── Contracts/
|
||||
│ ├── IOpenTelemetryIngestor.cs
|
||||
│ ├── IOpenTelemetryProvider.cs
|
||||
│ ├── IOpenTelemetryRedactor.cs
|
||||
│ ├── IOpenTelemetrySourceRegistry.cs
|
||||
│ ├── IOpenTelemetryStore.cs
|
||||
│ └── IOpenTelemetryLiveFeed.cs
|
||||
├── Endpoints/OpenTelemetry/
|
||||
│ ├── CollectorConfiguration/Endpoint.cs
|
||||
│ ├── Logs/Endpoint.cs
|
||||
│ ├── Metrics/Endpoint.cs
|
||||
│ ├── Resources/Endpoint.cs
|
||||
│ ├── Storage/Endpoint.cs
|
||||
│ ├── Trace/Endpoint.cs
|
||||
│ └── Traces/Endpoint.cs
|
||||
├── Extensions/
|
||||
├── Features/OpenTelemetryFeature.cs
|
||||
├── Ingestion/
|
||||
│ ├── Grpc/
|
||||
│ └── HttpProtobuf/
|
||||
├── Models/
|
||||
├── Options/OpenTelemetryDiagnosticsOptions.cs
|
||||
├── Permissions/OpenTelemetryPermissions.cs
|
||||
├── Providers/InMemory/
|
||||
├── RealTime/OpenTelemetryHub.cs
|
||||
├── Services/
|
||||
└── ShellFeatures/OpenTelemetryFeature.cs
|
||||
|
||||
test/unit/
|
||||
└── Elsa.Diagnostics.OpenTelemetry.UnitTests/
|
||||
|
||||
test/integration/
|
||||
└── Elsa.Diagnostics.OpenTelemetry.IntegrationTests/
|
||||
```
|
||||
|
||||
**Structure Decision**: Add a new diagnostics module parallel to `Elsa.Diagnostics.StructuredLogs` and `Elsa.Diagnostics.ConsoleLogs`. The module owns collector, normalization, storage, APIs, and live contracts only.
|
||||
|
||||
## Phase 0 Output
|
||||
|
||||
See [research.md](./research.md).
|
||||
|
||||
Resolved decisions:
|
||||
|
||||
- Core module is the OTLP collector and diagnostics API boundary.
|
||||
- Existing `Elsa.Workflows.Core` instrumentation remains the producer.
|
||||
- Do not port historical `Elsa.OpenTelemetry` producer middleware from `elsa-extensions`.
|
||||
- Support HTTP/protobuf as required; support gRPC when host/dependencies enable it and advertise disabled metadata otherwise.
|
||||
- Use bounded in-memory storage in v1.
|
||||
- Keep OTLP logs separate from `Elsa.Diagnostics.StructuredLogs`.
|
||||
- Require protection for non-loopback ingestion.
|
||||
|
||||
## Phase 1 Output
|
||||
|
||||
- [data-model.md](./data-model.md)
|
||||
- [contracts/rest-api.md](./contracts/rest-api.md)
|
||||
- [contracts/otlp-ingest.md](./contracts/otlp-ingest.md)
|
||||
- [contracts/signalr-hub.md](./contracts/signalr-hub.md)
|
||||
- [contracts/provider-contract.md](./contracts/provider-contract.md)
|
||||
- [quickstart.md](./quickstart.md)
|
||||
|
||||
## Post-Design Constitution Re-Check
|
||||
|
||||
| Principle | Verdict | Post-design evidence |
|
||||
|-----------|---------|----------------------|
|
||||
| I. Modular Architecture | PASS | Contracts and source tree keep all Core OpenTelemetry diagnostics behavior in one focused module. |
|
||||
| II. Composition & Extensibility | PASS | Provider/store/live-feed/redactor/ingestor contracts isolate real variability points. |
|
||||
| III. Convention-Driven Design | PASS | Endpoint and type names follow diagnostics module conventions. |
|
||||
| IV. Async & Pipeline Execution | PASS | Provider, store, endpoint, and hub contracts are async/cancellation-aware. |
|
||||
| V. Testing Discipline | PASS | Tasks include unit and integration tests for each buildable slice. |
|
||||
| VI. Trunk-Based Development | PASS | Core slice remains independently reviewable from Studio. |
|
||||
| VII. Simplicity, SRP, DRY & KISS | PASS | V1 avoids persistence and producer instrumentation rewrites. |
|
||||
|
||||
## Phase 2 Handoff
|
||||
|
||||
Use [tasks.md](./tasks.md) as the implementation backlog. Suggested order:
|
||||
|
||||
1. Create module/test projects, solution entries, options, permissions, feature, and shell feature.
|
||||
2. Add models and shared provider contracts.
|
||||
3. Add HTTP/protobuf ingestion, normalization, redaction, resource registry, bounded store, and provider facade.
|
||||
4. Add REST APIs, SignalR hub, collector configuration, and security checks.
|
||||
5. Add optional gRPC wrappers and disabled metadata handling.
|
||||
6. Add docs, sample host wiring if needed, and run targeted tests/builds.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
No constitution violations identified.
|
||||
72
specs/008-diagnostics-otel/quickstart.md
Normal file
72
specs/008-diagnostics-otel/quickstart.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Quickstart: Diagnostics OpenTelemetry
|
||||
|
||||
## Enable Core Diagnostics
|
||||
|
||||
```csharp
|
||||
services.AddElsa(elsa =>
|
||||
{
|
||||
elsa.UseOpenTelemetryDiagnostics(options =>
|
||||
{
|
||||
options.HttpEndpointPath = "/elsa/otlp/v1";
|
||||
options.ApiKey = builder.Configuration["Diagnostics:OpenTelemetry:ApiKey"];
|
||||
options.TraceCapacity = 2_000;
|
||||
options.SpanCapacity = 10_000;
|
||||
options.MetricPointCapacity = 20_000;
|
||||
options.LogRecordCapacity = 10_000;
|
||||
options.ResourceCapacity = 500;
|
||||
options.SubscriberChannelCapacity = 1_000;
|
||||
options.MaxHttpRequestBodySize = 10 * 1024 * 1024;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Configure Elsa Workflow Telemetry Export
|
||||
|
||||
```csharp
|
||||
services.AddOpenTelemetry()
|
||||
.WithTracing(builder => builder
|
||||
.AddSource("Elsa.Workflows")
|
||||
.AddOtlpExporter())
|
||||
.WithMetrics(builder => builder
|
||||
.AddMeter("Elsa.Workflows")
|
||||
.AddOtlpExporter());
|
||||
```
|
||||
|
||||
Development HTTP/protobuf environment:
|
||||
|
||||
```text
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5000/elsa/otlp/v1
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
OTEL_SERVICE_NAME=elsa-api
|
||||
OTEL_RESOURCE_ATTRIBUTES=service.instance.id=local-elsa-api,deployment.environment=development
|
||||
OTEL_BSP_SCHEDULE_DELAY=1000
|
||||
OTEL_BLRP_SCHEDULE_DELAY=1000
|
||||
OTEL_METRIC_EXPORT_INTERVAL=1000
|
||||
```
|
||||
|
||||
When the collector is reachable from non-loopback addresses, configure the server-side API key and send only the configured header name/value through OTLP exporter headers. Collector configuration must advertise required header names but never secret values.
|
||||
|
||||
gRPC senders should only use gRPC when collector configuration advertises a gRPC endpoint:
|
||||
|
||||
```text
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
dotnet test test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj
|
||||
dotnet test test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Run a workflow and confirm a workflow trace appears through `/diagnostics/opentelemetry/traces/search` within 2 seconds.
|
||||
- Query `/diagnostics/opentelemetry/traces/{traceId}` and confirm workflow/activity spans are parented and ordered.
|
||||
- Query collector configuration and confirm HTTP metadata, nullable/disabled gRPC metadata, and required header names.
|
||||
- Attempt non-loopback ingestion without the configured API key and confirm rejection.
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
This feature does not add durable OTEL persistence, vendor exporters, Studio UI, alerting, Kubernetes/Docker APIs, or historical `Elsa.OpenTelemetry` producer middleware. Production deployments should usually export to a standard OpenTelemetry Collector or observability platform unless Elsa collector capacity and security are deliberately configured.
|
||||
54
specs/008-diagnostics-otel/research.md
Normal file
54
specs/008-diagnostics-otel/research.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Research: Diagnostics OpenTelemetry
|
||||
|
||||
## Decision: Core owns the collector/API boundary
|
||||
|
||||
**Rationale**: OTLP ingestion must happen in an ASP.NET Core backend, not in Studio. Core can authenticate diagnostics APIs, protect ingestion, normalize protobuf payloads, and stream live updates.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Studio collector: rejected because browser/Studio hosts cannot reliably expose OTLP endpoints.
|
||||
- External collector only: rejected because this feature needs a first-party local diagnostics story.
|
||||
|
||||
## Decision: Preserve existing workflow instrumentation
|
||||
|
||||
**Rationale**: Current `Elsa.Workflows.Core` already emits `Elsa.Workflows` spans and metrics through `System.Diagnostics`. This module should consume/export that telemetry through OTLP rather than re-instrument workflow execution.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Port `elsa-extensions` `Elsa.OpenTelemetry`: rejected because it is producer-side middleware, overlaps current Core instrumentation, and includes trace-boundary behavior that mutates ambient `Activity.Current`.
|
||||
- Move instrumentation into diagnostics: rejected because workflow instrumentation is valuable even when diagnostics collector is not installed.
|
||||
|
||||
## Decision: HTTP/protobuf required, gRPC optional
|
||||
|
||||
**Rationale**: HTTP/protobuf fits existing ASP.NET Core endpoint patterns and common deployment routes. gRPC is useful for SDK defaults but depends on host configuration and package choices, so it should be enabled when available and represented honestly in collector metadata.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- gRPC only: rejected because it complicates proxying and local HTTP testing.
|
||||
- Always return a gRPC endpoint: rejected because it misleads clients when the host does not expose gRPC.
|
||||
|
||||
## Decision: Bounded in-memory v1 storage
|
||||
|
||||
**Rationale**: Traces, metrics, and logs can be high volume. A bounded in-memory default matches the existing diagnostics modules and keeps v1 focused.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Durable storage now: rejected because trace and metric schemas need a deliberate follow-up provider design.
|
||||
- Raw OTLP payload storage only: rejected because diagnostics clients need filtered, assembled trace and metric models.
|
||||
|
||||
## Decision: OTLP logs stay separate from Structured Logs
|
||||
|
||||
**Rationale**: OTLP logs can come from arbitrary resources and use OTEL schemas. `Elsa.Diagnostics.StructuredLogs` owns first-party `ILogger` capture. Correlation should use trace/span IDs, not shared storage.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- Merge OTLP logs into Structured Logs: rejected because schemas, retention, permissions, and source semantics differ.
|
||||
|
||||
## Decision: Explicit non-loopback protection
|
||||
|
||||
**Rationale**: OTLP ingestion can expose sensitive application metadata and create high traffic. Loopback development can be low friction, but remote ingestion needs an API key header or equivalent configured protection.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- No OTLP sender auth: rejected as unsafe.
|
||||
- Require interactive user auth for OTLP senders: rejected because SDKs usually support headers, not browser auth.
|
||||
158
specs/008-diagnostics-otel/spec.md
Normal file
158
specs/008-diagnostics-otel/spec.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Feature Specification: Diagnostics OpenTelemetry
|
||||
|
||||
**Feature Branch**: `008-diagnostics-otel`
|
||||
**Created**: 2026-05-25
|
||||
**Status**: Draft
|
||||
**Input**: Core-owned specification for the backend side of the OpenTelemetry diagnostics PRD coordinated with `elsa-studio/specs/008-diagnostics-otel`.
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-05-25
|
||||
|
||||
- Q: What default capacity and overflow policy should the Core in-memory OTEL store use? -> A: Configurable bounded defaults with drop-oldest per signal.
|
||||
- Q: How should Core decide when OTLP ingestion requires an API key? -> A: Require the configured header for any non-loopback request or non-loopback collector binding.
|
||||
- Q: How should live SignalR subscriptions behave under backpressure? -> A: Use bounded per-connection queues, drop oldest updates, and report dropped counts.
|
||||
- Q: Which workflow attributes are canonical for Elsa trace correlation? -> A: Preserve existing `Elsa.Workflows` semantic tags; do not add producer middleware or mutate ambient activity context.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Collect OpenTelemetry from Elsa services (Priority: P1)
|
||||
|
||||
An operator enables OpenTelemetry diagnostics on an Elsa backend and receives workflow traces, activity spans, metrics, resources, and correlated OTLP logs through a first-party diagnostics backend.
|
||||
|
||||
**Why this priority**: Studio cannot visualize telemetry until Core can collect, normalize, secure, and expose it.
|
||||
|
||||
**Independent Test**: Enable the Core OpenTelemetry diagnostics feature, run a workflow with multiple activities, export telemetry to the local diagnostics collector, and verify recent resources, traces, spans, metrics, and logs through backend diagnostics APIs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the diagnostics OpenTelemetry feature is enabled, **When** workflow execution emits spans and metrics from `Elsa.Workflows`, **Then** Core stores recent telemetry with resource identity, trace/span IDs, timing, status, and workflow metadata.
|
||||
2. **Given** a standard OpenTelemetry SDK posts OTLP traces, metrics, or logs to the HTTP/protobuf collector endpoint, **When** the payload is accepted, **Then** Core normalizes the telemetry into queryable diagnostics models.
|
||||
3. **Given** telemetry contains sensitive attributes matching configured redaction rules, **When** Core stores or streams it, **Then** sensitive values are redacted before provider boundaries.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Serve Trace Investigation APIs (Priority: P1)
|
||||
|
||||
Studio and other authenticated diagnostics clients can query recent traces and retrieve a trace detail model with ordered spans, resource metadata, OTLP logs, and storage diagnostics.
|
||||
|
||||
**Why this priority**: Trace investigation is the main workflow troubleshooting path and must be backend-filtered before Studio renders a waterfall.
|
||||
|
||||
**Independent Test**: Seed or ingest telemetry for a workflow trace and call the trace search/detail APIs with filters for service, resource, trace ID, workflow instance ID, status, text, and time range.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a trace contains workflow and activity spans, **When** a diagnostics client requests trace detail, **Then** Core returns parent/child span data ordered for a waterfall view.
|
||||
2. **Given** filters include workflow instance ID or trace ID, **When** trace search is executed, **Then** only matching trace summaries are returned.
|
||||
3. **Given** the caller lacks the OpenTelemetry diagnostics view permission, **When** the caller requests trace APIs or live updates, **Then** Core denies the request without exposing telemetry.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Serve Metrics and OTLP Logs (Priority: P2)
|
||||
|
||||
An authenticated diagnostics client can inspect recent OpenTelemetry metric instruments, bounded metric points, OTLP log records, and overflow diagnostics.
|
||||
|
||||
**Why this priority**: Metrics and OTLP logs complement traces and validate that all OpenTelemetry signals are flowing.
|
||||
|
||||
**Independent Test**: Ingest metrics and OTLP logs for multiple resources, query them by resource, instrument, severity, trace/span ID, text, and time range, and verify capacity diagnostics.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** metrics are emitted by multiple services, **When** a client filters by resource or instrument, **Then** Core returns only matching bounded series.
|
||||
2. **Given** a metric has high-cardinality attributes, **When** capacity is exceeded, **Then** Core drops according to configured policy and reports dropped point counts.
|
||||
3. **Given** OTLP logs include trace/span IDs, **When** logs are queried by trace/span, **Then** Core returns correlated OTLP log records without merging them into `Elsa.Diagnostics.StructuredLogs`.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Expose Collector Configuration and Secure Ingestion (Priority: P3)
|
||||
|
||||
A developer can discover active collector endpoints and configure .NET or non-.NET senders using standard OTEL environment variables, while Core protects non-loopback ingestion.
|
||||
|
||||
**Why this priority**: Elsa does not own an Aspire-style launcher, so Core must make active collector configuration discoverable and secure.
|
||||
|
||||
**Independent Test**: Request collector configuration, verify HTTP metadata and nullable/disabled gRPC metadata, configure a sample sender, and verify non-loopback ingestion requires the configured API key header.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the collector is enabled, **When** a diagnostics client requests collector configuration, **Then** Core returns HTTP endpoint metadata, any enabled gRPC endpoint metadata, required header names, and recommended non-secret environment variables.
|
||||
2. **Given** gRPC ingestion is unavailable, **When** configuration is requested, **Then** Core marks gRPC disabled instead of returning a misleading endpoint.
|
||||
3. **Given** OTLP ingestion is exposed beyond loopback, **When** a sender omits the required API key header, **Then** Core rejects the request.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- OTLP payloads include unsupported future fields or incomplete resource attributes.
|
||||
- Multiple services share `service.name` but have different `service.instance.id` values.
|
||||
- A trace arrives out of span order.
|
||||
- Metrics create more series or points than configured capacity allows.
|
||||
- Backend restarts and loses in-memory telemetry.
|
||||
- OTLP logs reference trace/span IDs for traces that have expired.
|
||||
- gRPC support is not available in the hosting configuration.
|
||||
- The caller has Studio access but not the OpenTelemetry diagnostics permission.
|
||||
- A sender posts from loopback while the collector is configured for loopback-only development.
|
||||
- A sender posts from a non-loopback address without the configured ingestion API key header.
|
||||
- A live client subscribes with filters that receive more updates than its per-connection queue allows.
|
||||
- The historical `Elsa.OpenTelemetry` extension package is present in another repository but must not be ported into this module.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: Core MUST introduce an opt-in module under `src/modules/Elsa.Diagnostics.OpenTelemetry`.
|
||||
- **FR-002**: The module MUST be separate from `Elsa.Diagnostics.StructuredLogs` and `Elsa.Diagnostics.ConsoleLogs`.
|
||||
- **FR-003**: The module MUST use existing `Elsa.Workflows` `ActivitySource` and `Meter` instrumentation as the producer of Elsa workflow telemetry.
|
||||
- **FR-004**: The module MUST NOT port the historical `Elsa.OpenTelemetry` producer-side middleware from `elsa-extensions` in v1.
|
||||
- **FR-005**: The module MUST accept OTLP HTTP/protobuf traces, metrics, and logs.
|
||||
- **FR-006**: The module SHOULD support OTLP gRPC ingestion when the host has gRPC support enabled; collector metadata MUST represent gRPC as disabled or null when unavailable.
|
||||
- **FR-007**: HTTP and gRPC ingestion, when both are enabled, MUST feed one shared ingestion contract for normalization, redaction, storage, and live publishing.
|
||||
- **FR-008**: The module MUST normalize telemetry into bounded queryable models for resources, traces, spans, metrics, and OTLP log records.
|
||||
- **FR-009**: The default store MUST be bounded in memory and MUST report dropped telemetry counts.
|
||||
- **FR-010**: Redaction MUST run before telemetry reaches provider storage or live subscribers.
|
||||
- **FR-011**: Resource identity MUST use OpenTelemetry resource attributes, including service name and service instance ID when present.
|
||||
- **FR-012**: Workflow and activity telemetry MUST preserve the existing `Elsa.Workflows` semantic attributes when present, including `workflow.instance.id`, `workflow.definition.id`, `workflow.definition.version`, `workflow.definition.version.id`, `workflow.status`, `workflow.substatus`, `workflow.faulted`, `workflow.parent.instance.id`, `workflow.correlation.id`, `workflow.activity.id`, `workflow.activity.name`, `workflow.activity.type`, `workflow.activity.version`, `workflow.activity.execution.id`, `workflow.activity.status`, `workflow.activity.outcome`, `workflow.activity.parent.execution.id`, `workflow.activity.scheduled.by.execution.id`, `workflow.activity.faulted`, `elsa.tenant.id`, and `exception.type`.
|
||||
- **FR-013**: The module MUST expose authenticated diagnostics APIs for resources, trace search, trace detail, metrics, OTLP logs, storage diagnostics, and collector configuration.
|
||||
- **FR-014**: The module MUST expose live updates through an authenticated SignalR hub.
|
||||
- **FR-015**: The module MUST enforce an OpenTelemetry diagnostics view permission for all diagnostics APIs and live connections.
|
||||
- **FR-016**: Loopback-only development ingestion MAY run without an API key only for loopback requests while the collector is bound to loopback; any non-loopback request or non-loopback collector binding MUST require an explicit API key header or equivalent configured protection.
|
||||
- **FR-017**: Collector configuration MUST expose standard OTEL environment variable metadata without exposing secret values.
|
||||
- **FR-018**: The module MUST document that production deployments should generally export to an external OpenTelemetry Collector or observability backend unless Elsa collector capacity and security are deliberately configured.
|
||||
- **FR-019**: OTLP logs MUST remain separate from `Elsa.Diagnostics.StructuredLogs`; correlation uses trace/span IDs rather than shared storage.
|
||||
- **FR-020**: The module MUST include tests for ingestion, normalization, redaction, bounded storage, permissions, live updates, collector configuration, and end-to-end workflow export-to-collector timing.
|
||||
- **FR-021**: In-memory storage defaults MUST be configurable and MUST start with bounded development defaults of at least 500 resources, 2,000 traces, 10,000 spans, 20,000 metric points, 10,000 OTLP log records, and 1,000 queued live updates per subscriber; when a capacity is exceeded, the oldest item in that signal-specific buffer is dropped and the relevant dropped count is incremented.
|
||||
- **FR-022**: Search APIs MUST return deterministic, bounded result sets with caller-specified limits capped by server options; default ordering is newest receive time first for searches and parent/child chronological ordering for trace detail spans.
|
||||
- **FR-023**: Live SignalR subscriptions MUST use bounded per-connection queues, update filters in place, drop oldest queued updates on overflow, and publish dropped-update diagnostics without disconnecting healthy subscribers.
|
||||
- **FR-024**: The diagnostics module MUST NOT start new workflow or activity spans itself and MUST NOT mutate `Activity.Current`; trace production remains owned by existing `Elsa.Workflows.Core` instrumentation.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Telemetry Resource**: Resource/service identity derived from OTEL resource attributes.
|
||||
- **Telemetry Trace**: Trace summary grouped by trace ID.
|
||||
- **Telemetry Span**: Timed operation inside a trace with attributes, events, links, and status.
|
||||
- **Metric Instrument**: Metric name, unit, description, type, resource, and bounded series.
|
||||
- **Metric Point**: Recent data point for a metric series.
|
||||
- **OTLP Log Record**: Log record received through OTLP and correlated by trace/span IDs.
|
||||
- **Collector Configuration**: Active endpoint, protocol, security, and environment-variable metadata.
|
||||
- **Storage Diagnostics**: Capacity and dropped telemetry counters.
|
||||
- **Telemetry Subscription**: Live SignalR subscription with filters and connection state.
|
||||
- **Storage Capacity Policy**: Configurable per-signal limits and drop-oldest overflow behavior for in-memory telemetry.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: A local Core host with OpenTelemetry diagnostics enabled receives workflow trace spans and exposes them through diagnostics APIs within 2 seconds of workflow execution.
|
||||
- **SC-002**: Core accepts representative OTLP HTTP/protobuf traces, metrics, and logs from standard SDK payloads and returns normalized diagnostics models.
|
||||
- **SC-003**: Trace detail APIs return at least 100 ordered spans for one trace without unbounded memory growth.
|
||||
- **SC-004**: Metric APIs return at least 20 instruments and 1,000 recent points while honoring configured capacity.
|
||||
- **SC-005**: Unauthorized API and hub calls are denied without exposing telemetry.
|
||||
- **SC-006**: Sensitive configured attribute names and text patterns are redacted before stored telemetry is returned.
|
||||
- **SC-007**: Collector configuration reports HTTP metadata, nullable/disabled gRPC metadata, and required header names accurately.
|
||||
- **SC-008**: Capacity tests prove each signal-specific buffer drops oldest telemetry, increments dropped counts, and keeps queries bounded when defaults are exceeded.
|
||||
- **SC-009**: SignalR tests prove subscriber overflow drops oldest queued live updates, reports dropped-update counts, and keeps the connection usable.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- `Elsa.Workflows.Core` keeps first-party workflow tracing and metrics instrumentation.
|
||||
- Durable OpenTelemetry persistence is a later provider feature.
|
||||
- The Studio feature has its own spec in `elsa-studio/specs/008-diagnostics-otel`.
|
||||
- Core and Studio contracts should remain aligned, but Core is the source of truth for ingestion, permissions, normalization, and API behavior.
|
||||
- Default in-memory capacities are development-friendly starting points, not production sizing guidance.
|
||||
123
specs/008-diagnostics-otel/tasks.md
Normal file
123
specs/008-diagnostics-otel/tasks.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Tasks: Diagnostics OpenTelemetry
|
||||
|
||||
**Input**: Design documents from `/specs/008-diagnostics-otel/`
|
||||
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md), [data-model.md](./data-model.md), [contracts/](./contracts/), [quickstart.md](./quickstart.md)
|
||||
|
||||
**Tests**: Required for ingestion, normalization, redaction, bounded storage, permissions, live updates, collector configuration, and workflow export-to-collector timing.
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
- [X] T001 Create Core project skeleton at `src/modules/Elsa.Diagnostics.OpenTelemetry/Elsa.Diagnostics.OpenTelemetry.csproj`
|
||||
- [X] T002 Create unit test project at `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj`
|
||||
- [X] T003 Create integration test project at `test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj`
|
||||
- [X] T004 Add project and test projects to `Elsa.sln`
|
||||
- [X] T005 [P] Add initial module README at `src/modules/Elsa.Diagnostics.OpenTelemetry/README.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational
|
||||
|
||||
- [X] T006 Create telemetry resource/span/trace/metric/log/storage models in `src/modules/Elsa.Diagnostics.OpenTelemetry/Models`
|
||||
- [X] T007 Create filter/result DTOs for resources, traces, metrics, logs, and collector configuration in `src/modules/Elsa.Diagnostics.OpenTelemetry/Models`
|
||||
- [X] T008 Create provider, store, live feed, ingestor, redactor, and source registry contracts in `src/modules/Elsa.Diagnostics.OpenTelemetry/Contracts`
|
||||
- [X] T009 Create options in `src/modules/Elsa.Diagnostics.OpenTelemetry/Options/OpenTelemetryDiagnosticsOptions.cs` with configurable capacity defaults, ingestion security, and live subscriber queue settings
|
||||
- [X] T010 Create permission constants in `src/modules/Elsa.Diagnostics.OpenTelemetry/Permissions/OpenTelemetryPermissions.cs`
|
||||
- [X] T011 Create feature and shell feature in `src/modules/Elsa.Diagnostics.OpenTelemetry/Features/OpenTelemetryFeature.cs` and `src/modules/Elsa.Diagnostics.OpenTelemetry/ShellFeatures/OpenTelemetryFeature.cs`
|
||||
- [X] T012 Create DI and endpoint mapping extensions in `src/modules/Elsa.Diagnostics.OpenTelemetry/Extensions`
|
||||
- [X] T013 Add README note that historical `Elsa.OpenTelemetry` producer middleware is not ported in v1 in `src/modules/Elsa.Diagnostics.OpenTelemetry/README.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Collect OpenTelemetry from Elsa services (Priority: P1) MVP
|
||||
|
||||
**Goal**: Receive OTLP payloads, normalize, redact, store bounded telemetry, and expose basic diagnostics queries.
|
||||
|
||||
**Independent Test**: Ingest OTLP traces/metrics/logs and run an Elsa workflow export-to-collector integration test.
|
||||
|
||||
- [X] T014 [P] [US1] Add OTLP trace normalization tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Ingestion/OtlpTraceNormalizerTests.cs`
|
||||
- [X] T015 [P] [US1] Add OTLP metric normalization tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Ingestion/OtlpMetricNormalizerTests.cs`
|
||||
- [X] T016 [P] [US1] Add OTLP log normalization tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Ingestion/OtlpLogNormalizerTests.cs`
|
||||
- [X] T017 [P] [US1] Add redaction tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Services/OpenTelemetryRedactorTests.cs`
|
||||
- [X] T018 [P] [US1] Add bounded store/drop-count tests for default capacities, drop-oldest overflow, and deterministic query limits in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Providers/InMemoryOpenTelemetryStoreTests.cs`
|
||||
- [X] T019 [P] [US1] Add HTTP/protobuf ingestion and workflow export-to-collector timing tests that assert existing `Elsa.Workflows` tags are preserved in `test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OtlpHttpIngestionTests.cs`
|
||||
- [X] T020 [US1] Implement HTTP/protobuf request parsing in `src/modules/Elsa.Diagnostics.OpenTelemetry/Ingestion/HttpProtobuf`
|
||||
- [X] T021 [US1] Implement shared OTLP normalization services in `src/modules/Elsa.Diagnostics.OpenTelemetry/Ingestion`
|
||||
- [X] T022 [US1] Implement redactor in `src/modules/Elsa.Diagnostics.OpenTelemetry/Services/OpenTelemetryRedactor.cs`
|
||||
- [X] T023 [US1] Implement source registry in `src/modules/Elsa.Diagnostics.OpenTelemetry/Services/OpenTelemetrySourceRegistry.cs`
|
||||
- [X] T024 [US1] Implement bounded in-memory store in `src/modules/Elsa.Diagnostics.OpenTelemetry/Providers/InMemory/InMemoryOpenTelemetryStore.cs`
|
||||
- [X] T025 [US1] Implement provider facade in `src/modules/Elsa.Diagnostics.OpenTelemetry/Services/DefaultOpenTelemetryProvider.cs`
|
||||
- [X] T026 [US1] Implement HTTP/protobuf endpoint mapping in `src/modules/Elsa.Diagnostics.OpenTelemetry/Extensions/EndpointRouteBuilderExtensions.cs`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Serve Trace Investigation APIs (Priority: P1)
|
||||
|
||||
**Goal**: Provide authenticated trace search/detail APIs and live updates.
|
||||
|
||||
**Independent Test**: Query traces by service/resource/trace/workflow/status/time and retrieve ordered span detail.
|
||||
|
||||
- [X] T027 [P] [US2] Add trace search/detail provider tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Providers/OpenTelemetryTraceQueryTests.cs`
|
||||
- [X] T028 [P] [US2] Add API authorization tests in `test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OpenTelemetryAuthorizationTests.cs`
|
||||
- [X] T029 [P] [US2] Add SignalR hub tests for authorization, filter updates, per-subscriber queue overflow, drop-oldest behavior, and dropped-update diagnostics in `test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OpenTelemetryHubTests.cs`
|
||||
- [X] T030 [US2] Implement trace search and trace detail query logic in `src/modules/Elsa.Diagnostics.OpenTelemetry/Providers/InMemory/InMemoryOpenTelemetryStore.cs`
|
||||
- [X] T031 [US2] Implement resource search endpoint in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Resources/Endpoint.cs`
|
||||
- [X] T032 [US2] Implement trace search and detail endpoints in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Traces/Endpoint.cs` and `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Trace/Endpoint.cs`
|
||||
- [X] T033 [US2] Implement live feed in `src/modules/Elsa.Diagnostics.OpenTelemetry/Providers/InMemory/InMemoryOpenTelemetryLiveFeed.cs`
|
||||
- [X] T034 [US2] Implement SignalR hub in `src/modules/Elsa.Diagnostics.OpenTelemetry/RealTime/OpenTelemetryHub.cs`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Serve Metrics and OTLP Logs (Priority: P2)
|
||||
|
||||
**Goal**: Provide bounded metric and OTLP log search APIs with overflow diagnostics.
|
||||
|
||||
**Independent Test**: Ingest metrics/logs, query by filters, and verify dropped counts.
|
||||
|
||||
- [X] T035 [P] [US3] Add metric query tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Providers/OpenTelemetryMetricQueryTests.cs`
|
||||
- [X] T036 [P] [US3] Add OTLP log query tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Providers/OtlpLogQueryTests.cs`
|
||||
- [X] T037 [US3] Implement metric instrument and point query support in `src/modules/Elsa.Diagnostics.OpenTelemetry/Providers/InMemory/InMemoryOpenTelemetryStore.cs`
|
||||
- [X] T038 [US3] Implement OTLP log query support in `src/modules/Elsa.Diagnostics.OpenTelemetry/Providers/InMemory/InMemoryOpenTelemetryStore.cs`
|
||||
- [X] T039 [US3] Implement metrics endpoint in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Metrics/Endpoint.cs`
|
||||
- [X] T040 [US3] Implement OTLP logs endpoint in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Logs/Endpoint.cs`
|
||||
- [X] T041 [US3] Implement storage diagnostics endpoint in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Storage/Endpoint.cs`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - Expose Collector Configuration and Secure Ingestion (Priority: P3)
|
||||
|
||||
**Goal**: Provide collector configuration metadata and protect non-loopback ingestion.
|
||||
|
||||
**Independent Test**: Query configuration and verify API key enforcement.
|
||||
|
||||
- [X] T042 [P] [US4] Add collector configuration tests in `test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Services/CollectorConfigurationTests.cs`
|
||||
- [X] T043 [P] [US4] Add ingestion security tests for loopback-only no-key development, non-loopback request rejection without API key, and non-secret collector metadata in `test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OtlpIngestionSecurityTests.cs`
|
||||
- [X] T044 [US4] Implement collector configuration provider in `src/modules/Elsa.Diagnostics.OpenTelemetry/Services/CollectorConfigurationProvider.cs`
|
||||
- [X] T045 [US4] Implement collector configuration endpoint in `src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/CollectorConfiguration/Endpoint.cs`
|
||||
- [X] T046 [US4] Implement API key header validation in `src/modules/Elsa.Diagnostics.OpenTelemetry/Ingestion`
|
||||
- [X] T047 [US4] Add option-gated gRPC wrappers and disabled metadata handling in `src/modules/Elsa.Diagnostics.OpenTelemetry/Ingestion/Grpc`
|
||||
- [X] T048 [US4] Update workflow OpenTelemetry docs in `doc/wiki/opentelemetry-workflows.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Polish and Verification
|
||||
|
||||
- [X] T049 [P] Update module README with routes, permissions, security, scope boundaries, and historical extension decision in `src/modules/Elsa.Diagnostics.OpenTelemetry/README.md`
|
||||
- [X] T050 [P] Verify no production code references historical producer middleware by running `rg "Elsa.OpenTelemetry|UseWorkflowExecutionTracing|UseActivityExecutionTracing" src test`
|
||||
- [X] T051 Run `dotnet test test/unit/Elsa.Diagnostics.OpenTelemetry.UnitTests/Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj`
|
||||
- [X] T052 Run `dotnet test test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj`
|
||||
- [X] T053 Run `dotnet build Elsa.sln`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 precedes all work.
|
||||
- Phase 2 blocks all user stories.
|
||||
- US1 is the MVP backend collector slice.
|
||||
- US2 can begin after Phase 2 with seeded store data; full validation depends on US1.
|
||||
- US3 depends on US1 storage and normalization.
|
||||
- US4 depends on endpoint mapping from US1.
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep Studio UI tasks in `elsa-studio/specs/008-diagnostics-otel/tasks.md`.
|
||||
- Do not port historical `Elsa.OpenTelemetry` middleware in this feature.
|
||||
- Durable OTEL persistence and vendor exporters are out of scope.
|
||||
34
specs/010-workflow-json-hardening/checklists/requirements.md
Normal file
34
specs/010-workflow-json-hardening/checklists/requirements.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Specification Quality Checklist: Workflow JSON Type Hardening
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-05-29
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Validation passed. The issue explicitly requires a dedicated workflow serialization type registry or resolver; the specification names this capability as a required product behavior while deferring implementation details to planning.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Contract: Workflow JSON Type Identifiers
|
||||
|
||||
## Identifier Rules
|
||||
|
||||
- New workflow JSON writes aliases when a type is registered with a workflow JSON alias.
|
||||
- Compatibility reads accept registered aliases and registered legacy names.
|
||||
- Unknown CLR names are rejected.
|
||||
- Abstract, interface, open generic, and inappropriate collection targets are rejected for polymorphic object materialization.
|
||||
- Supported collection aliases are limited to known collection wrappers closed over registered element types.
|
||||
|
||||
## Incident Strategy Descriptor
|
||||
|
||||
`GET /descriptors/incident-strategies` returns `typeName` values from the shared serialization type registry.
|
||||
|
||||
Clients should persist or submit returned `typeName` values unchanged. During the compatibility window, existing CLR names registered as legacy identifiers remain accepted when workflow JSON is read.
|
||||
20
specs/010-workflow-json-hardening/data-model.md
Normal file
20
specs/010-workflow-json-hardening/data-model.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Data Model: Workflow JSON Type Hardening
|
||||
|
||||
## SerializationTypeOptions
|
||||
|
||||
- Stores workflow JSON aliases mapped to concrete types.
|
||||
- Stores optional legacy names mapped to the same concrete types.
|
||||
- Provides default primitive and JSON island aliases needed by workflow payloads.
|
||||
- Lives in `Elsa.Common` so non-workflow serialization layers can share the same trust boundary.
|
||||
|
||||
## SerializationTypeRegistry
|
||||
|
||||
- Runtime registry built from `SerializationTypeOptions`.
|
||||
- Resolves aliases and registered legacy names to types.
|
||||
- Lists registered types for compatibility resolution.
|
||||
- Returns preferred aliases for writing new workflow JSON and public descriptor values.
|
||||
|
||||
## Workflow Type Identifier
|
||||
|
||||
- Alias: preferred stable identifier for new workflow JSON.
|
||||
- Legacy name: supported compatibility identifier for previously persisted workflow JSON or older clients.
|
||||
88
specs/010-workflow-json-hardening/plan.md
Normal file
88
specs/010-workflow-json-hardening/plan.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Implementation Plan: Workflow JSON Type Hardening
|
||||
|
||||
**Branch**: `codex/7541-workflow-json-hardening` | **Date**: 2026-05-29 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `/specs/010-workflow-json-hardening/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit-plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
Reintroduce workflow JSON hardening through a workflow-specific type registry and resolver. Existing registered legacy CLR names remain readable, new workflow JSON emits aliases when available, and public descriptor APIs use the same identifier contract they accept.
|
||||
|
||||
## Technical Context
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||
for the project. The structure here is presented in advisory capacity to guide
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**Language/Version**: C# latest with nullable reference types, multi-targeting through existing project settings
|
||||
**Primary Dependencies**: System.Text.Json, Elsa feature/module infrastructure, existing expression registry for expression-only behavior
|
||||
**Storage**: Existing workflow JSON payloads only; no schema changes
|
||||
**Testing**: Targeted `dotnet test` for workflow core/runtime/api test projects
|
||||
**Target Platform**: Elsa server/library consumers on supported .NET target frameworks
|
||||
**Project Type**: Modular .NET library and API modules
|
||||
**Performance Goals**: Type lookup remains dictionary-based and does not add reflection scans to normal serialization
|
||||
**Constraints**: No arbitrary CLR type loading; compatibility only for explicitly registered workflow JSON legacy names
|
||||
**Scale/Scope**: Workflow definitions, workflow state, trigger/bookmark payloads, incident strategy descriptor contract, and module/extension registrations
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
PASS. The change stays inside existing workflow modules, adds one explicit extensibility point for serialization trust, preserves public compatibility, and includes focused tests. No new persistence provider or unrelated module is introduced.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/speckit-plan command output)
|
||||
├── research.md # Phase 0 output (/speckit-plan command)
|
||||
├── data-model.md # Phase 1 output (/speckit-plan command)
|
||||
├── quickstart.md # Phase 1 output (/speckit-plan command)
|
||||
├── contracts/ # Phase 1 output (/speckit-plan command)
|
||||
└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
|
||||
for this feature. Delete unused options and expand the chosen structure with
|
||||
real paths (e.g., apps/admin, packages/something). The delivered plan must
|
||||
not include Option labels.
|
||||
-->
|
||||
|
||||
```text
|
||||
src/modules/Elsa.Workflows.Core/
|
||||
├── Contracts/
|
||||
├── Extensions/
|
||||
├── Options/
|
||||
├── Serialization/
|
||||
└── Features/
|
||||
|
||||
src/modules/Elsa.Workflows.Runtime/
|
||||
├── Features/
|
||||
├── ShellFeatures/
|
||||
└── Services/
|
||||
|
||||
src/modules/Elsa.Workflows.Api/
|
||||
└── Endpoints/IncidentStrategies/
|
||||
|
||||
test/unit/Elsa.Workflows.Core.UnitTests/
|
||||
test/unit/Elsa.Workflows.Runtime.UnitTests/
|
||||
test/integration/Elsa.Workflows.IntegrationTests/
|
||||
```
|
||||
|
||||
**Structure Decision**: Extend the existing workflow core serialization surface and update runtime/API registrations in place. Tests stay in the nearest existing workflow test projects.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
11
specs/010-workflow-json-hardening/quickstart.md
Normal file
11
specs/010-workflow-json-hardening/quickstart.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Quickstart: Workflow JSON Type Hardening
|
||||
|
||||
1. Register built-in workflow JSON aliases through workflow features.
|
||||
2. Register module payload aliases through each module that serializes workflow payloads.
|
||||
3. Verify `TypeJsonConverter` and `PolymorphicObjectConverter` use the shared serialization registry, not expression options.
|
||||
4. Run targeted tests:
|
||||
|
||||
```sh
|
||||
dotnet test test/unit/Elsa.Workflows.Core.UnitTests/Elsa.Workflows.Core.UnitTests.csproj --filter SerializationTypeResolverTests
|
||||
dotnet test test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj --filter WorkflowRuntimeFeatureTests
|
||||
```
|
||||
25
specs/010-workflow-json-hardening/research.md
Normal file
25
specs/010-workflow-json-hardening/research.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Research: Workflow JSON Type Hardening
|
||||
|
||||
## Decision: Dedicated Workflow JSON Registry
|
||||
|
||||
Use a workflow-specific registry and options object for `TypeJsonConverter`, `PolymorphicObjectConverter`, workflow state serialization, bookmark payload serialization, trigger comparison, hashing, and descriptor contracts.
|
||||
|
||||
**Rationale**: The issue explicitly rejects using `ExpressionOptions` as the workflow JSON trust boundary. A dedicated registry makes the allowed workflow JSON surface explicit while leaving expression aliases available for expression evaluation and designer variable metadata.
|
||||
|
||||
**Alternatives considered**: Reusing `IWellKnownTypeRegistry` was rejected because it is fed by `ExpressionOptions`. Direct `Type.GetType` fallback was rejected because it can load arbitrary CLR names.
|
||||
|
||||
## Decision: Explicit Legacy Compatibility Names
|
||||
|
||||
Compatibility reads accept aliases and registered legacy names, including simple assembly-qualified names, full assembly-qualified names for registered types, and supported collection wrappers over registered element types.
|
||||
|
||||
**Rationale**: Existing persisted workflows may contain CLR names, but the trust boundary must be the registration list, not assembly probing.
|
||||
|
||||
**Alternatives considered**: A broad assembly allow-list was rejected for this slice because it is harder to reason about and can accidentally expose unrelated types from trusted assemblies.
|
||||
|
||||
## Decision: Alias-First Public Descriptor Contract
|
||||
|
||||
Incident strategy descriptors should emit the workflow JSON alias for registered strategies while keeping legacy CLR names readable on input during the compatibility window.
|
||||
|
||||
**Rationale**: The reported dropdown failure came from descriptors returning CLR names while hardened reads expected aliases. Emitting aliases aligns new clients with hardened serialization without breaking old payloads.
|
||||
|
||||
**Alternatives considered**: Keeping CLR names in descriptors was rejected because it perpetuates the inconsistent contract.
|
||||
103
specs/010-workflow-json-hardening/spec.md
Normal file
103
specs/010-workflow-json-hardening/spec.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Feature Specification: Workflow JSON Type Hardening
|
||||
|
||||
**Feature Branch**: `codex/7541-workflow-json-hardening`
|
||||
**Created**: 2026-05-29
|
||||
**Status**: Draft
|
||||
**Input**: User description: "GitHub issue #7541: Redo workflow JSON type hardening with dedicated alias design"
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Load Existing Workflows Safely (Priority: P1)
|
||||
|
||||
Operators can upgrade Elsa without losing access to persisted workflows whose JSON contains existing type identifiers, including CLR type names emitted by earlier versions.
|
||||
|
||||
**Why this priority**: Backward compatibility is required before hardening can be reintroduced without breaking production workflows.
|
||||
|
||||
**Independent Test**: Load representative persisted workflow JSON that contains legacy type names and verify the workflow can be read while unknown or unsafe type identifiers are rejected.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a persisted workflow that references a known legacy type name, **When** the workflow is deserialized after upgrade, **Then** the workflow loads successfully.
|
||||
2. **Given** a persisted workflow that references an unknown or untrusted type name, **When** the workflow is deserialized, **Then** the workflow is rejected with a clear failure instead of resolving arbitrary types.
|
||||
3. **Given** a persisted workflow that contains abstract, interface, open generic, or collection type identifiers where a concrete workflow type is required, **When** the workflow is deserialized, **Then** the identifier is rejected.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Use Consistent Type Identifiers in APIs (Priority: P2)
|
||||
|
||||
API consumers receive consistent type identifiers for workflow-facing options such as incident handling strategies and can submit those identifiers back without relying on implementation-specific names.
|
||||
|
||||
**Why this priority**: The reported incident strategy dropdown failure came from inconsistent contracts between API descriptors and hardened workflow JSON reads.
|
||||
|
||||
**Independent Test**: Request descriptor data for incident strategies, select a returned option, and verify a workflow or workflow definition using that option can be accepted and read.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** API descriptor data for incident handling strategies, **When** the options are returned, **Then** every option uses the documented workflow type identifier contract.
|
||||
2. **Given** a client submits a supported incident strategy identifier from the descriptor payload, **When** the workflow payload is processed, **Then** the identifier resolves to the intended strategy.
|
||||
3. **Given** older clients submit legacy CLR identifiers during the compatibility window, **When** the workflow payload is processed, **Then** supported legacy identifiers continue to resolve.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Register Workflow-Serializable Types Explicitly (Priority: P3)
|
||||
|
||||
Module and extension authors can explicitly register the types and legacy names that are valid in workflow serialization payloads without using expression configuration as the trust boundary.
|
||||
|
||||
**Why this priority**: Hardening must be extensible for built-in modules, custom workflow types, runtime payloads, and third-party extensions.
|
||||
|
||||
**Independent Test**: Register a custom workflow-facing type and legacy identifier, then verify alias-based payloads and supported legacy payloads resolve while unrelated types do not.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a module registers a workflow-serializable type with an alias, **When** workflow JSON references the alias, **Then** the type resolves successfully.
|
||||
2. **Given** a module registers a supported legacy name for a workflow-serializable type, **When** compatibility JSON references the legacy name, **Then** the type resolves successfully.
|
||||
3. **Given** a type is registered only for expression use, **When** workflow JSON references it, **Then** workflow JSON resolution does not accept it unless it is also registered for workflow serialization.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Legacy JSON references a CLR type moved between assemblies or renamed after earlier persistence.
|
||||
- Payloads reference unknown, untrusted, abstract, interface, open generic, array, dictionary, or collection types.
|
||||
- JSON islands contain values that resemble type metadata but should remain normal JSON data.
|
||||
- Runtime trigger and bookmark payloads include polymorphic values that need the same trust model as workflow definitions.
|
||||
- Custom workflow types are registered by host applications or extensions after core services are configured.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The system MUST keep workflow JSON type resolution separate from expression type alias configuration.
|
||||
- **FR-002**: The system MUST provide a dedicated registry or resolver for workflow-serializable type identifiers.
|
||||
- **FR-003**: The system MUST allow built-in modules, extensions, and host applications to register workflow-serializable aliases.
|
||||
- **FR-004**: The system MUST allow supported legacy type names to be registered for backward-compatible reads.
|
||||
- **FR-005**: The system MUST document which legacy identifiers remain readable during the compatibility window and how unsupported identifiers fail.
|
||||
- **FR-006**: The system MUST reject unknown, untrusted, abstract, open generic, interface, and inappropriate collection type identifiers when resolving workflow JSON types.
|
||||
- **FR-007**: The system MUST continue loading existing workflows that reference supported CLR type names during the compatibility window.
|
||||
- **FR-008**: The system MUST serialize new workflow-facing payloads with the documented type identifier contract where the payload belongs to that contract.
|
||||
- **FR-009**: The system MUST keep public API payloads that expose workflow type identifiers internally consistent for request and response flows.
|
||||
- **FR-010**: The system MUST cover the incident handling strategy option flow with a regression test.
|
||||
- **FR-011**: The system MUST cover persisted workflow JSON, alias JSON, runtime trigger or bookmark payloads, JSON islands, and custom workflow types with tests.
|
||||
- **FR-012**: The system MUST document the security model, trust boundaries, and migration behavior before hardened resolution is reintroduced.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Workflow Type Identifier**: A stable value used in workflow-facing JSON or API payloads to identify an allowed type.
|
||||
- **Workflow-Serializable Type Registration**: A trusted registration that maps a workflow type identifier and optional legacy names to a concrete allowed type.
|
||||
- **Compatibility Window**: The supported period or mode in which selected legacy CLR type names remain readable.
|
||||
- **Public Type Identifier Contract**: The documented request and response behavior for API payloads that expose workflow type identifiers.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: All representative legacy workflow JSON fixtures covered by tests load successfully when they use supported legacy identifiers.
|
||||
- **SC-002**: Tests demonstrate rejection for unknown, untrusted, abstract, interface, open generic, and inappropriate collection type identifiers.
|
||||
- **SC-003**: Incident strategy descriptor and submit flows pass a regression test that uses the same identifier contract in both directions.
|
||||
- **SC-004**: New workflow JSON hardening tests run without depending on expression type alias configuration.
|
||||
- **SC-005**: Documentation explains the trust model and compatibility behavior clearly enough for module authors to register workflow-serializable types without reading implementation code.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The compatibility window accepts known Elsa workflow-related CLR type names and explicitly registered host or extension legacy names, not arbitrary CLR resolution.
|
||||
- Public API type identifier changes can be transitional: aliases are preferred for new payloads while supported legacy values remain readable.
|
||||
- Existing workflow JSON fixtures are sufficient to represent persisted workflow compatibility risks, with new fixtures added where gaps are found.
|
||||
- Cloud vault, external migration tooling, and data store schema changes are outside this feature unless existing tests prove they are required.
|
||||
125
specs/010-workflow-json-hardening/tasks.md
Normal file
125
specs/010-workflow-json-hardening/tasks.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Tasks: Workflow JSON Type Hardening
|
||||
|
||||
**Input**: Design documents from `/specs/010-workflow-json-hardening/`
|
||||
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
|
||||
|
||||
**Tests**: Required by the feature specification for compatibility, rejection behavior, incident strategies, runtime payloads, JSON islands, and custom workflow types.
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Confirm the clean worktree and documentation context.
|
||||
|
||||
- [x] T001 Verify the dedicated worktree status in `/Users/sipke/Projects/Elsa/elsa-core-7541`
|
||||
- [x] T002 Update Spec Kit agent context in `/Users/sipke/Projects/Elsa/elsa-core-7541/AGENTS.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Add the dedicated workflow JSON trust boundary before changing user-facing behavior.
|
||||
|
||||
- [x] T003 Add serialization type registry contracts in `src/modules/Elsa.Common/Serialization/ISerializationTypeRegistry.cs`
|
||||
- [x] T004 Add serialization type options in `src/modules/Elsa.Common/Serialization/SerializationTypeOptions.cs`
|
||||
- [x] T005 Add serialization type registry implementation in `src/modules/Elsa.Common/Serialization/SerializationTypeRegistry.cs`
|
||||
- [x] T006 Add workflow JSON registration extensions in `src/modules/Elsa.Common/Extensions/SerializationTypeOptionsExtensions.cs`
|
||||
- [x] T007 Update workflow JSON resolver to use the dedicated registry in `src/modules/Elsa.Common/Serialization/SerializationTypeResolver.cs`
|
||||
- [x] T008 Update workflow type converters to use the dedicated registry in `src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverterFactory.cs`, and `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs`
|
||||
|
||||
**Checkpoint**: Workflow JSON converters no longer depend on `ExpressionOptions`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Load Existing Workflows Safely (Priority: P1) MVP
|
||||
|
||||
**Goal**: Existing registered CLR type names remain readable while unsafe names fail.
|
||||
|
||||
**Independent Test**: Deserialize legacy workflow JSON/type payloads with registered names and verify unsafe identifiers are rejected.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [x] T009 [US1] Update resolver tests for dedicated registry compatibility in `test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs`
|
||||
- [x] T010 [US1] Add regression coverage for expression-only aliases not being accepted by workflow JSON in `test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [x] T011 [US1] Register core workflow JSON aliases and legacy names in `src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs` and `src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs`
|
||||
- [x] T012 [US1] Update workflow serializers and hashing to consume `ISerializationTypeRegistry` in `src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs`, and `src/modules/Elsa.Workflows.Core/Services/Hasher.cs`
|
||||
|
||||
**Checkpoint**: User Story 1 can be validated independently.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Use Consistent Type Identifiers in APIs (Priority: P2)
|
||||
|
||||
**Goal**: Incident strategy descriptors emit the same workflow type identifiers that workflow JSON accepts.
|
||||
|
||||
**Independent Test**: Descriptor output for incident strategies returns aliases and supported legacy identifiers remain readable.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [x] T013 [US2] Add incident strategy descriptor regression tests in `test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/IncidentStrategies/ListTests.cs`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [x] T014 [US2] Register incident strategy aliases and legacy names in `src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs` and `src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs`
|
||||
- [x] T015 [US2] Emit workflow JSON aliases from incident strategy descriptors in `src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs`
|
||||
- [x] T016 [US2] Update client model documentation for identifier semantics in `src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs`
|
||||
|
||||
**Checkpoint**: User Story 2 can be validated independently.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Register Workflow-Serializable Types Explicitly (Priority: P3)
|
||||
|
||||
**Goal**: Modules and host applications register workflow JSON types without using expression options.
|
||||
|
||||
**Independent Test**: Runtime and extension payloads resolve from workflow JSON registrations while expression-only aliases do not.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [x] T017 [US3] Update runtime feature tests for workflow JSON registrations in `test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs`
|
||||
- [x] T018 [US3] Update runtime trigger comparer tests for the dedicated registry in `test/unit/Elsa.Workflows.Runtime.UnitTests/Comparers/WorkflowTriggerEqualityComparerTests.cs`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [x] T019 [US3] Move runtime workflow payload registration to workflow JSON options in `src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs`, `src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs`, and `src/modules/Elsa.Workflows.Runtime/ShellFeatures/WorkflowRuntimeFeature.cs`
|
||||
- [x] T020 [US3] Move module payload registrations to workflow JSON options in `src/modules/Elsa.Http/Features/HttpFeature.cs`, `src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs`, `src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs`, `src/modules/Elsa.Resilience/Features/ResilienceFeature.cs`, `src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs`, `src/modules/Elsa.Alterations/Features/AlterationsFeature.cs`, `src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs`, and `src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs`
|
||||
- [x] T021 [US3] Update runtime trigger comparison to use `ISerializationTypeRegistry` in `src/modules/Elsa.Workflows.Runtime/Comparers/WorkflowTriggerEqualityComparer.cs` and `src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs`
|
||||
|
||||
**Checkpoint**: User Story 3 can be validated independently.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Documentation and validation.
|
||||
|
||||
- [x] T022 Add workflow JSON hardening documentation in `doc/wiki/workflow-core.md`
|
||||
- [x] T023 Run targeted workflow core unit tests with `dotnet test test/unit/Elsa.Workflows.Core.UnitTests/Elsa.Workflows.Core.UnitTests.csproj --filter SerializationTypeResolverTests`
|
||||
- [x] T024 Run targeted workflow runtime unit tests with `dotnet test test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj --filter WorkflowRuntimeFeatureTests`
|
||||
- [x] T025 Review changed files and ensure `.specify/feature.json` and Spec Kit artifacts are correct
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
- **Setup**: T001-T002 first.
|
||||
- **Foundational**: T003-T008 block all user stories.
|
||||
- **US1**: T009-T012 produces the MVP compatibility slice.
|
||||
- **US2**: T013-T016 depends on the registry from Foundational and may run after US1.
|
||||
- **US3**: T017-T021 depends on the registry from Foundational and may run after US1.
|
||||
- **Polish**: T022-T025 after implementation.
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
- Test updates in T009, T013, T017, and T018 can be developed in parallel after T003-T008.
|
||||
- Module registration moves in T020 can be parallelized by module after the extension API exists.
|
||||
- Documentation T022 can run after the contract behavior is finalized.
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
1. Establish the dedicated registry and switch converters.
|
||||
2. Preserve legacy compatibility for existing workflow JSON.
|
||||
3. Align incident strategy API descriptors with the registry.
|
||||
4. Move runtime/module payload registrations off expression options.
|
||||
5. Validate with targeted tests and documentation.
|
||||
|
|
@ -5,9 +5,14 @@
|
|||
<PackageReference Include="Nuplane" />
|
||||
<PackageReference Include="Nuplane.Loading" />
|
||||
<PackageReference Include="Nuplane.Sources.Directory" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.ConsoleLogs\Elsa.Diagnostics.ConsoleLogs.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.OpenTelemetry\Elsa.Diagnostics.OpenTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite\Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.StructuredLogs\Elsa.Diagnostics.StructuredLogs.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ using ConsoleLogStreaming.Core.Capture;
|
|||
using CShells.AspNetCore.Configuration;
|
||||
using CShells.AspNetCore.Extensions;
|
||||
using CShells.DependencyInjection;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Extensions;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Elsa.ModularServer.Web;
|
||||
using Elsa.ModularServer.Web.Catalog;
|
||||
using Elsa.ShellFeatures;
|
||||
|
|
@ -12,23 +10,44 @@ using Elsa.Workflows.Management.ShellFeatures;
|
|||
using Elsa.Workflows.Runtime.Distributed.ShellFeatures;
|
||||
using Elsa.Workflows.Runtime.ShellFeatures;
|
||||
using Elsa.Workflows.ShellFeatures;
|
||||
using Elsa.Workflows.Telemetry;
|
||||
using Nuplane;
|
||||
using Nuplane.Loading.Hosting.Builder;
|
||||
using Nuplane.Sources.Directory.Configuration;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Logs;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
// Install the console stream tee as early as possible — before WebApplication.CreateBuilder and
|
||||
// any logger provider construction — so that downstream loggers (Microsoft.Extensions.Logging.Console,
|
||||
// Kestrel/Hosting loggers, etc.) capture our tee writer instead of the raw Console.Out / Console.Error.
|
||||
ConsoleStreamHook.Install();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
var configuration = builder.Configuration;
|
||||
var serviceVersion = typeof(Program).Assembly.GetName().Version?.ToString();
|
||||
|
||||
// Console output is a single process-wide resource; the capture pipeline is owned by the root host so
|
||||
// every shell's diagnostics endpoints (REST + SignalR) read from the same in-memory ring buffer.
|
||||
services.AddConsoleLogsHost();
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
logging.ParseStateValues = true;
|
||||
logging.SetResourceBuilder(CreateOpenTelemetryResource(builder.Environment.ApplicationName, serviceVersion));
|
||||
logging.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "logs"));
|
||||
});
|
||||
|
||||
services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource.AddService(builder.Environment.ApplicationName, serviceVersion: serviceVersion))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddSource(WorkflowInstrumentation.ActivitySourceName)
|
||||
.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "traces")))
|
||||
.WithMetrics(metrics => metrics
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddMeter(WorkflowInstrumentation.MeterName)
|
||||
.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "metrics")));
|
||||
var nuplaneConfiguration = configuration.GetSection("Nuplane");
|
||||
|
||||
services.AddNuplane(nuplaneConfiguration, nuplane =>
|
||||
|
|
@ -71,3 +90,39 @@ app.UseAuthentication();
|
|||
app.UseAuthorization();
|
||||
app.MapSampleCatalog();
|
||||
app.Run();
|
||||
|
||||
static ResourceBuilder CreateOpenTelemetryResource(string serviceName, string? serviceVersion)
|
||||
{
|
||||
return ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: serviceVersion);
|
||||
}
|
||||
|
||||
static void ConfigureDiagnosticsOtlpExporter(OtlpExporterOptions options, IConfiguration configuration, string signal)
|
||||
{
|
||||
var protocol = configuration["Diagnostics:OpenTelemetry:Exporter:Protocol"];
|
||||
options.Protocol = protocol?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"grpc" => OtlpExportProtocol.Grpc,
|
||||
"http/protobuf" or "httpprotobuf" => OtlpExportProtocol.HttpProtobuf,
|
||||
_ => options.Protocol
|
||||
};
|
||||
|
||||
var endpoint = configuration["Diagnostics:OpenTelemetry:Exporter:Endpoint"];
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
options.Endpoint = new Uri(GetSignalEndpoint(endpoint, signal, options.Protocol), UriKind.Absolute);
|
||||
}
|
||||
|
||||
static string GetSignalEndpoint(string endpoint, string signal, OtlpExportProtocol protocol)
|
||||
{
|
||||
if (protocol != OtlpExportProtocol.HttpProtobuf)
|
||||
return endpoint;
|
||||
|
||||
var trimmed = endpoint.TrimEnd('/');
|
||||
|
||||
if (trimmed.EndsWith($"/v1/{signal}", StringComparison.OrdinalIgnoreCase))
|
||||
return trimmed;
|
||||
|
||||
if (trimmed.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
return $"{trimmed}/{signal}";
|
||||
|
||||
return $"{trimmed}/v1/{signal}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5002",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "Elsa.ModularServer.Web",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Endpoint": "http://localhost:5002/elsa/otlp",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Protocol": "http/protobuf"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
|
|
@ -16,7 +19,34 @@
|
|||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7294;http://localhost:5002",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "Elsa.ModularServer.Web",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Endpoint": "https://localhost:7294/elsa/otlp",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Protocol": "http/protobuf"
|
||||
}
|
||||
},
|
||||
"http-aspire": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5002",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "Elsa.ModularServer.Web",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Endpoint": "http://localhost:4317",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Protocol": "grpc"
|
||||
}
|
||||
},
|
||||
"https-aspire": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7294;http://localhost:5002",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "Elsa.ModularServer.Web",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Endpoint": "http://localhost:4317",
|
||||
"Diagnostics__OpenTelemetry__Exporter__Protocol": "grpc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning",
|
||||
"Nuplane": "Warning"
|
||||
"Nuplane": "Warning",
|
||||
"CShells": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"FormatterOptions": {
|
||||
|
|
@ -12,6 +13,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Diagnostics": {
|
||||
"OpenTelemetry": {
|
||||
"Exporter": {
|
||||
"Endpoint": "http://localhost:5002/elsa/otlp",
|
||||
"Protocol": "http/protobuf"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"CShells": {
|
||||
"Shells": {
|
||||
|
|
@ -44,6 +53,11 @@
|
|||
},
|
||||
"StructuredLogs": {},
|
||||
"ConsoleLogs": {},
|
||||
"OpenTelemetry": {},
|
||||
"Alterations": {},
|
||||
"SqliteAlterationsPersistence": {
|
||||
"ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared"
|
||||
},
|
||||
"SqliteStructuredLogPersistence": {
|
||||
"ConnectionString": "Data Source=elsa_logs.db;Cache=Shared"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ public interface IActivityExecutionsApi
|
|||
/// <param name="id">The ID of the activity execution.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The activity execution.</returns>
|
||||
[Get("/activity-executions/{id}")]
|
||||
Task<ActivityExecutionRecord?> GetAsync(string id, CancellationToken cancellationToken = default);
|
||||
[Get("/activity-executions/by-id")]
|
||||
Task<ActivityExecutionRecord?> GetAsync([Query] string id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the call stack (execution chain) for a given activity execution.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ namespace Elsa.Api.Client.Resources.IncidentStrategies.Models;
|
|||
/// <summary>
|
||||
/// Represents an incident strategy.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The .NET type name of the strategy.</param>
|
||||
/// <param name="TypeName">The workflow JSON type identifier of the strategy.</param>
|
||||
/// <param name="DisplayName">The display name of the strategy.</param>
|
||||
/// <param name="Description">The description of the strategy.</param>
|
||||
public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
|
||||
public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Mediator.CommandStrategies;
|
||||
|
|
@ -13,7 +14,15 @@ public class BackgroundStrategy : ICommandStrategy
|
|||
public async Task<TResult> ExecuteAsync<TResult>(CommandStrategyContext context)
|
||||
{
|
||||
var commandsChannel = context.ServiceProvider.GetRequiredService<ICommandsChannel>();
|
||||
await commandsChannel.Writer.WriteAsync(context.CommandContext, context.CancellationToken);
|
||||
var commandContext = context.CommandContext;
|
||||
var queuedContext = new CommandContext(
|
||||
commandContext.Command,
|
||||
CommandStrategy.Default,
|
||||
commandContext.ResultType,
|
||||
commandContext.Headers,
|
||||
commandContext.ServiceProvider,
|
||||
CancellationToken.None);
|
||||
await commandsChannel.Writer.WriteAsync(queuedContext, context.CancellationToken);
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,15 +48,28 @@ public static class DependencyInjectionExtensions
|
|||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddMediatorHostedServices(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddMediatorBackgroundChannels()
|
||||
.AddHostedService<JobRunnerHostedService>()
|
||||
.AddHostedService<BackgroundCommandSenderHostedService>()
|
||||
.AddHostedService<BackgroundEventPublisherHostedService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds mediator background processing channels to the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMediatorBackgroundChannels(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddSingleton<INotificationsChannel, NotificationsChannel>()
|
||||
.AddSingleton<ICommandsChannel, CommandsChannel>()
|
||||
.AddSingleton<IJobsChannel, JobsChannel>()
|
||||
.AddSingleton<IJobQueue, JobQueue>()
|
||||
.AddHostedService<JobRunnerHostedService>()
|
||||
.AddHostedService<BackgroundCommandSenderHostedService>()
|
||||
.AddHostedService<BackgroundEventPublisherHostedService>();
|
||||
.AddSingleton<BackgroundCommandProcessor>()
|
||||
.AddSingleton<BackgroundNotificationProcessor>()
|
||||
.AddSingleton<BackgroundJobProcessor>()
|
||||
;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -225,4 +238,4 @@ public static class DependencyInjectionExtensions
|
|||
private static Channel<T> CreateChannel<T>() => Channel.CreateUnbounded<T>(new UnboundedChannelOptions());
|
||||
private static ChannelReader<T> CreateChannelReader<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Reader;
|
||||
private static ChannelWriter<T> CreateChannelWriter<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Writer;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,20 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Mediator.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which commands can be sent, executing each received command.
|
||||
/// Hosted service adapter for <see cref="BackgroundCommandProcessor"/>.
|
||||
/// </summary>
|
||||
public class BackgroundCommandSenderHostedService : BackgroundService
|
||||
public class BackgroundCommandSenderHostedService(BackgroundCommandProcessor processor) : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly ICommandsChannel _commandsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly List<Channel<CommandContext>> _outputs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BackgroundCommandSenderHostedService(IOptions<MediatorOptions> options, ICommandsChannel commandsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundCommandSenderHostedService> logger)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundCommandSenderHostedService"/> class.
|
||||
/// </summary>
|
||||
public BackgroundCommandSenderHostedService(BackgroundCommandProcessor processor, int workerCount) : this(processor)
|
||||
{
|
||||
_workerCount = options.Value.CommandWorkerCount;
|
||||
_commandsChannel = commandsChannel; // The shared input channel for all commands
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new(_workerCount); // Prepare a list to hold worker-specific channels
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0; // Used for round-robin distribution of work
|
||||
|
||||
// Set up worker channels and start background tasks for each worker
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<CommandContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task that processes commands from this worker's channel
|
||||
_ = ReadOutputAsync(output, cancellationToken);
|
||||
}
|
||||
|
||||
// Main dispatcher loop: read from the input channel and distribute to worker channels
|
||||
try
|
||||
{
|
||||
await foreach (var commandContext in _commandsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(commandContext, cancellationToken);
|
||||
// Round-robin distribution - move to next worker
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
|
||||
// If the input channel is completed, complete all worker channels
|
||||
foreach (var output in _outputs)
|
||||
output.Writer.Complete();
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<CommandContext> output, CancellationToken cancellationToken)
|
||||
{
|
||||
// Worker task: process commands from the worker's channel
|
||||
try
|
||||
{
|
||||
await foreach (var commandContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a fresh scope for each command to ensure proper service lifetime
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
// Decouple from the caller's CancellationToken.
|
||||
// We use only the service's cancellationToken (the background worker's lifetime)
|
||||
// to ensure that dispatched workflows are processed even if the original
|
||||
// HTTP request or triggering context has timed out.
|
||||
await commandSender.SendAsync(
|
||||
commandContext.Command,
|
||||
CommandStrategy.Default,
|
||||
commandContext.Headers,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log errors but continue processing other commands
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) => processor.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,111 +1,20 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Mediator.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which notifications can be sent, publishing each received notification.
|
||||
/// Hosted service adapter for <see cref="BackgroundNotificationProcessor"/>.
|
||||
/// </summary>
|
||||
public class BackgroundEventPublisherHostedService : BackgroundService
|
||||
public class BackgroundEventPublisherHostedService(BackgroundNotificationProcessor processor) : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly INotificationsChannel _notificationsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly List<Channel<NotificationContext>> _outputs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BackgroundEventPublisherHostedService(IOptions<MediatorOptions> options, INotificationsChannel notificationsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundEventPublisherHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.NotificationWorkerCount;
|
||||
_notificationsChannel = notificationsChannel;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new(_workerCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Index to round-robin distribute notifications across worker channels
|
||||
var index = 0;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var notificationSender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
|
||||
|
||||
// Create multiple output channels and start worker tasks for parallel processing
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<NotificationContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task to process notifications from this output channel
|
||||
_ = ReadOutputAsync(output, notificationSender, cancellationToken);
|
||||
}
|
||||
|
||||
var channelReader = _notificationsChannel.Reader;
|
||||
|
||||
// Continuously read notifications from the input channel and distribute them to worker channels
|
||||
// using round-robin distribution for load balancing
|
||||
try
|
||||
{
|
||||
await foreach (var notification in channelReader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(notification, cancellationToken);
|
||||
// Move to the next worker in a circular fashion
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
|
||||
// When the input channel is completed, complete all output channels
|
||||
foreach (var output in _outputs)
|
||||
{
|
||||
output.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes notifications from an output channel asynchronously.
|
||||
/// Initializes a new instance of the <see cref="BackgroundEventPublisherHostedService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="output">The channel to read notifications from</param>
|
||||
/// <param name="notificationSender">The service used to send notifications</param>
|
||||
/// <param name="cancellationToken">Cancellation token from the hosted service</param>
|
||||
private async Task ReadOutputAsync(Channel<NotificationContext> output, INotificationSender notificationSender, CancellationToken cancellationToken)
|
||||
public BackgroundEventPublisherHostedService(BackgroundNotificationProcessor processor, int workerCount) : this(processor)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var notificationContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = notificationContext.Notification;
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notificationContext.CancellationToken);
|
||||
await notificationSender.SendAsync(notification, NotificationStrategy.Sequential, linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
_logger.LogDebug(e, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) => processor.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,13 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Options;
|
||||
using Elsa.Mediator.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// A hosted service that runs jobs.
|
||||
/// Hosted service adapter for <see cref="BackgroundJobProcessor"/>.
|
||||
/// </summary>
|
||||
public class JobRunnerHostedService : BackgroundService
|
||||
public class JobRunnerHostedService(BackgroundJobProcessor processor) : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly IJobsChannel _jobsChannel;
|
||||
private readonly ILogger<JobRunnerHostedService> _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public JobRunnerHostedService(IOptions<MediatorOptions> options, IJobsChannel jobsChannel, ILogger<JobRunnerHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.JobWorkerCount;
|
||||
_jobsChannel = jobsChannel;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Create an array of worker tasks to process jobs in parallel
|
||||
var workers = new Task[_workerCount];
|
||||
|
||||
// Start multiple workers (tasks) to process jobs concurrently
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
workers[i] = ProcessJobsAsync(stoppingToken);
|
||||
|
||||
// Wait for all worker tasks to complete (typically when the application is shutting down)
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously processes jobs from the job channel until cancellation is requested.
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">Cancellation token from the hosted service</param>
|
||||
private async Task ProcessJobsAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Process all jobs from the channel until it's completed or cancellation is requested
|
||||
await foreach (var jobItem in _jobsChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, jobItem.CancellationTokenSource.Token);
|
||||
await jobItem.Action(linkedTokenSource.Token);
|
||||
_logger.LogInformation("Worker {CurrentTaskId} processed job {JobId}", Task.CurrentId, jobItem.JobId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Job {JobId} was canceled", jobItem.JobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Job {JobId} failed", jobItem.JobId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Notify that the job has completed (whether successfully or not)
|
||||
jobItem.OnJobCompleted(jobItem.JobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) => processor.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Mediator.PublishingStrategies;
|
||||
|
|
@ -13,7 +14,13 @@ public class BackgroundProcessingStrategy : IEventPublishingStrategy
|
|||
public async Task PublishAsync(NotificationStrategyContext context)
|
||||
{
|
||||
var notificationsChannel = context.ServiceProvider.GetRequiredService<INotificationsChannel>();
|
||||
var notificationContext = context.NotificationContext;
|
||||
var queuedContext = new NotificationContext(
|
||||
notificationContext.Notification,
|
||||
NotificationStrategy.Sequential,
|
||||
notificationContext.ServiceProvider,
|
||||
notificationContext.CancellationToken);
|
||||
|
||||
await notificationsChannel.Writer.WriteAsync(context.NotificationContext, context.CancellationToken);
|
||||
await notificationsChannel.Writer.WriteAsync(queuedContext, context.CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
src/common/Elsa.Mediator/Services/BackgroundCommandProcessor.cs
Normal file
101
src/common/Elsa.Mediator/Services/BackgroundCommandProcessor.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which commands can be sent, executing each received command.
|
||||
/// </summary>
|
||||
public class BackgroundCommandProcessor
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly ICommandsChannel _commandsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundCommandProcessor"/> class.
|
||||
/// </summary>
|
||||
public BackgroundCommandProcessor(IOptions<MediatorOptions> options, ICommandsChannel commandsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundCommandProcessor> logger)
|
||||
{
|
||||
_workerCount = options.Value.CommandWorkerCount;
|
||||
_commandsChannel = commandsChannel;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the processor until cancellation is requested.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0;
|
||||
var outputs = new List<Channel<CommandContext>>(_workerCount);
|
||||
var workers = new List<Task>(_workerCount);
|
||||
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<CommandContext>();
|
||||
outputs.Add(output);
|
||||
workers.Add(ReadOutputAsync(output, cancellationToken));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var commandContext in _commandsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = outputs[index];
|
||||
await output.Writer.WriteAsync(commandContext, cancellationToken);
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the command queue");
|
||||
}
|
||||
|
||||
foreach (var output in outputs)
|
||||
output.Writer.Complete();
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<CommandContext> output, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var commandContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, commandContext.CancellationToken);
|
||||
|
||||
await commandSender.SendAsync(
|
||||
commandContext.Command,
|
||||
commandContext.CommandStrategy,
|
||||
commandContext.Headers,
|
||||
linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
_logger.LogDebug(e, "An operation was cancelled while processing the command queue");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the command queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the command queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/common/Elsa.Mediator/Services/BackgroundJobProcessor.cs
Normal file
71
src/common/Elsa.Mediator/Services/BackgroundJobProcessor.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which jobs can be sent, executing each received job.
|
||||
/// </summary>
|
||||
public class BackgroundJobProcessor
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly IJobsChannel _jobsChannel;
|
||||
private readonly ILogger<BackgroundJobProcessor> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundJobProcessor"/> class.
|
||||
/// </summary>
|
||||
public BackgroundJobProcessor(IOptions<MediatorOptions> options, IJobsChannel jobsChannel, ILogger<BackgroundJobProcessor> logger)
|
||||
{
|
||||
_workerCount = options.Value.JobWorkerCount;
|
||||
_jobsChannel = jobsChannel;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the processor until cancellation is requested.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var workers = new Task[_workerCount];
|
||||
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
workers[i] = ProcessJobsAsync(cancellationToken);
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
private async Task ProcessJobsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var jobItem in _jobsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, jobItem.CancellationTokenSource.Token);
|
||||
await jobItem.Action(linkedTokenSource.Token);
|
||||
_logger.LogInformation("Worker {CurrentTaskId} processed job {JobId}", Task.CurrentId, jobItem.JobId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Job {JobId} was canceled", jobItem.JobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Job {JobId} failed", jobItem.JobId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
jobItem.OnJobCompleted(jobItem.JobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the job queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which notifications can be sent, publishing each received notification.
|
||||
/// </summary>
|
||||
public class BackgroundNotificationProcessor
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly INotificationsChannel _notificationsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundNotificationProcessor"/> class.
|
||||
/// </summary>
|
||||
public BackgroundNotificationProcessor(IOptions<MediatorOptions> options, INotificationsChannel notificationsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundNotificationProcessor> logger)
|
||||
{
|
||||
_workerCount = options.Value.NotificationWorkerCount;
|
||||
_notificationsChannel = notificationsChannel;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the processor until cancellation is requested.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0;
|
||||
var outputs = new List<Channel<NotificationContext>>(_workerCount);
|
||||
var workers = new List<Task>(_workerCount);
|
||||
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<NotificationContext>();
|
||||
outputs.Add(output);
|
||||
workers.Add(ReadOutputAsync(output, cancellationToken));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var notification in _notificationsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = outputs[index];
|
||||
await output.Writer.WriteAsync(notification, cancellationToken);
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the notification queue");
|
||||
}
|
||||
|
||||
foreach (var output in outputs)
|
||||
output.Writer.Complete();
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<NotificationContext> output, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var notificationSender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
|
||||
|
||||
await foreach (var notificationContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = notificationContext.Notification;
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notificationContext.CancellationToken);
|
||||
await notificationSender.SendAsync(notification, notificationContext.NotificationStrategy, linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
_logger.LogDebug(e, "An operation was cancelled while processing the notification queue");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the notification queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug(ex, "An operation was cancelled while processing the notification queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,12 @@ using Elsa.Alterations.Core.Stores;
|
|||
using Elsa.Alterations.Extensions;
|
||||
using Elsa.Alterations.Services;
|
||||
using Elsa.Alterations.Workflows;
|
||||
using Elsa.Expressions.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Workflows.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Alterations.Features;
|
||||
|
||||
|
|
@ -60,10 +61,10 @@ public class AlterationsFeature : FeatureBase
|
|||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.Configure<ExpressionOptions>(options =>
|
||||
Services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), typeof(AlterationPlanParams).GetSimpleAssemblyQualifiedName());
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
|
||||
});
|
||||
|
||||
Services.AddScoped<IAlterationPlanManager, AlterationPlanManager>();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
using CShells.FastEndpoints.Features;
|
||||
using CShells.Features;
|
||||
using Elsa.Alterations.Core.Contracts;
|
||||
using Elsa.Alterations.Core.Entities;
|
||||
using Elsa.Alterations.Core.Extensions;
|
||||
using Elsa.Alterations.Core.Models;
|
||||
using Elsa.Alterations.Core.Stores;
|
||||
using Elsa.Alterations.Extensions;
|
||||
using Elsa.Alterations.Services;
|
||||
using Elsa.Alterations.Workflows;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Options;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Alterations.ShellFeatures;
|
||||
|
||||
|
|
@ -16,9 +21,10 @@ namespace Elsa.Alterations.ShellFeatures;
|
|||
/// </summary>
|
||||
[ShellFeature(
|
||||
DisplayName = "Alterations",
|
||||
Description = "Provides workflow alteration capabilities for modifying running workflow instances")]
|
||||
Description = "Provides workflow alteration capabilities for modifying running workflow instances",
|
||||
DependsOn = ["ElsaFastEndpoints", "WorkflowRuntime"])]
|
||||
[UsedImplicitly]
|
||||
public class AlterationsFeature : IShellFeature
|
||||
public class AlterationsFeature : IFastEndpointsShellFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the factory for the alteration plan store.
|
||||
|
|
@ -37,6 +43,12 @@ public class AlterationsFeature : IShellFeature
|
|||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
|
||||
});
|
||||
|
||||
services.AddScoped<IAlterationPlanManager, AlterationPlanManager>();
|
||||
services.AddAlterations();
|
||||
services.AddAlterationsCore();
|
||||
|
|
@ -51,6 +63,7 @@ public class AlterationsFeature : IShellFeature
|
|||
services.AddScoped(AlterationPlanStoreFactory);
|
||||
services.AddScoped(AlterationJobStoreFactory);
|
||||
services.AddScoped(AlterationJobDispatcherFactory);
|
||||
|
||||
services.AddWorkflow<ExecuteAlterationPlanWorkflow>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,4 @@ public class OrderDefinition<T, TProp>
|
|||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Expression<Func<T, TProp>> KeySelector { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A best-effort textual representation of the key selector for cache-key generation.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? KeySelectorText => KeySelector?.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using Elsa.Common.Serialization;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="SerializationTypeOptions"/>.
|
||||
/// </summary>
|
||||
public static class SerializationTypeOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAlias<T>(this SerializationTypeOptions options, string alias) => options.RegisterTypeAlias(typeof(T), alias);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the type name as the preferred serialization alias.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAlias<T>(this SerializationTypeOptions options) => options.RegisterTypeAlias(typeof(T), typeof(T).Name);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAliasWithLegacyName(this SerializationTypeOptions options, Type type, string alias)
|
||||
{
|
||||
options.RegisterTypeAlias(type, alias);
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(type);
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAliasWithLegacyName<T>(this SerializationTypeOptions options, string alias) => options.AddTypeAliasWithLegacyName(typeof(T), alias);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as a compatibility identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName<T>(this SerializationTypeOptions options) => options.RegisterLegacySimpleAssemblyQualifiedName(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as a compatibility identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName(this SerializationTypeOptions options, Type type) => options.RegisterLegacySimpleAssemblyQualifiedName(type);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as the preferred alias for compatibility-only types.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddSimpleAssemblyQualifiedTypeAlias(this SerializationTypeOptions options, Type type)
|
||||
{
|
||||
return options.RegisterTypeAlias(type, type.GetSimpleAssemblyQualifiedName());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
|
|
@ -6,6 +8,17 @@ namespace Elsa.Extensions;
|
|||
/// </summary>
|
||||
public static class TypeExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly-qualified name of the type, without version, culture, and public key token information.
|
||||
/// </summary>
|
||||
public static string GetSimpleAssemblyQualifiedName(this Type type)
|
||||
{
|
||||
if (type is null) throw new ArgumentNullException(nameof(type));
|
||||
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, BuildSimplifiedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true of the type is generic, false otherwise.
|
||||
/// </summary>
|
||||
|
|
@ -54,4 +67,22 @@ public static class TypeExtensions
|
|||
{
|
||||
return type.IsPrimitive || type == typeof(decimal) || type == typeof(float) || type == typeof(double) || type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte) || type == typeof(uint) || type == typeof(ulong) || type == typeof(ushort) || type == typeof(sbyte);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildSimplifiedName(Type type)
|
||||
{
|
||||
var assemblyName = type.Assembly.GetName().Name;
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericTypeName = type.GetGenericTypeDefinition().FullName!;
|
||||
var backtickIndex = genericTypeName.IndexOf('`');
|
||||
var typeNameWithoutArity = genericTypeName[..backtickIndex];
|
||||
var arity = genericTypeName[backtickIndex..];
|
||||
var simplifiedGenericArguments = type.GetGenericArguments().Select(BuildSimplifiedName);
|
||||
|
||||
return $"{typeNameWithoutArity}{arity}[[{string.Join("],[", simplifiedGenericArguments)}]], {assemblyName}";
|
||||
}
|
||||
|
||||
return $"{type.FullName}, {assemblyName}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// A registry of types that may be resolved from serialization type identifiers.
|
||||
/// </summary>
|
||||
public interface ISerializationTypeRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a type with an alias.
|
||||
/// </summary>
|
||||
void RegisterType(Type type, string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the preferred alias for the specified type.
|
||||
/// </summary>
|
||||
bool TryGetAlias(Type type, out string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the type associated with the specified alias or legacy name.
|
||||
/// </summary>
|
||||
bool TryGetType(string alias, out Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registered types.
|
||||
/// </summary>
|
||||
IEnumerable<Type> ListTypes();
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Extensions;
|
||||
|
||||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Options for serialization type identifiers.
|
||||
/// </summary>
|
||||
public class SerializationTypeOptions
|
||||
{
|
||||
private readonly IDictionary<string, Type> _aliasTypeDictionary = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IDictionary<Type, string> _typeAliasDictionary = new Dictionary<Type, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SerializationTypeOptions"/> class.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions()
|
||||
{
|
||||
AliasTypeDictionary = new ReadOnlyDictionary<string, Type>(_aliasTypeDictionary);
|
||||
TypeAliasDictionary = new ReadOnlyDictionary<Type, string>(_typeAliasDictionary);
|
||||
|
||||
RegisterTypeAlias(typeof(short), "Int16");
|
||||
RegisterTypeAlias(typeof(int), "Int32");
|
||||
RegisterTypeAlias(typeof(long), "Int64");
|
||||
RegisterLegacyTypeName(typeof(long), "Long");
|
||||
RegisterTypeAlias(typeof(float), "Single");
|
||||
RegisterTypeAlias(typeof(object), "Object");
|
||||
RegisterTypeAlias(typeof(string), "String");
|
||||
RegisterTypeAlias(typeof(bool), "Boolean");
|
||||
RegisterTypeAlias(typeof(decimal), "Decimal");
|
||||
RegisterTypeAlias(typeof(double), "Double");
|
||||
RegisterTypeAlias(typeof(byte[]), "ByteArray");
|
||||
RegisterTypeAlias(typeof(Guid), nameof(Guid));
|
||||
RegisterTypeAlias(typeof(DateTime), nameof(DateTime));
|
||||
RegisterTypeAlias(typeof(DateTimeOffset), nameof(DateTimeOffset));
|
||||
RegisterTypeAlias(typeof(TimeSpan), nameof(TimeSpan));
|
||||
RegisterTypeAlias(typeof(Stream), nameof(Stream));
|
||||
RegisterTypeAlias(typeof(ExpandoObject), "JSON");
|
||||
RegisterTypeAlias(typeof(JsonElement), nameof(JsonElement));
|
||||
RegisterTypeAlias(typeof(JsonNode), nameof(JsonNode));
|
||||
RegisterTypeAlias(typeof(JsonObject), nameof(JsonObject));
|
||||
RegisterTypeAlias(typeof(JsonArray), nameof(JsonArray));
|
||||
RegisterTypeAlias(typeof(IDictionary<string, string>), "StringDictionary");
|
||||
RegisterTypeAlias(typeof(IDictionary<string, object>), "ObjectDictionary");
|
||||
RegisterTypeAlias(typeof(Dictionary<string, string>), "StringMap");
|
||||
RegisterTypeAlias(typeof(Dictionary<string, object>), "ObjectMap");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets aliases and legacy names keyed by identifier.
|
||||
/// </summary>
|
||||
public IDictionary<string, Type> AliasTypeDictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets preferred aliases keyed by type.
|
||||
/// </summary>
|
||||
public IDictionary<Type, string> TypeAliasDictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterTypeAlias(Type type, string alias)
|
||||
{
|
||||
_aliasTypeDictionary[alias] = type;
|
||||
_typeAliasDictionary[type] = alias;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a legacy serialization identifier for compatibility reads.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterLegacyTypeName(Type type, string typeName)
|
||||
{
|
||||
_aliasTypeDictionary[typeName] = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the type's simple assembly-qualified name as a legacy serialization identifier.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterLegacySimpleAssemblyQualifiedName(Type type) => RegisterLegacyTypeName(type, type.GetSimpleAssemblyQualifiedName());
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SerializationTypeRegistry : ISerializationTypeRegistry
|
||||
{
|
||||
private readonly Dictionary<string, Type> _aliasTypeDictionary = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<Type, string> _typeAliasDictionary = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default registry.
|
||||
/// </summary>
|
||||
public static ISerializationTypeRegistry CreateDefault()
|
||||
{
|
||||
return new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SerializationTypeRegistry"/> class.
|
||||
/// </summary>
|
||||
public SerializationTypeRegistry(IOptions<SerializationTypeOptions> options)
|
||||
{
|
||||
foreach (var entry in options.Value.AliasTypeDictionary)
|
||||
RegisterTypeName(entry.Value, entry.Key);
|
||||
|
||||
foreach (var entry in options.Value.TypeAliasDictionary)
|
||||
RegisterType(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterType(Type type, string alias)
|
||||
{
|
||||
_typeAliasDictionary[type] = alias;
|
||||
RegisterTypeName(type, alias);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetAlias(Type type, out string alias) => _typeAliasDictionary.TryGetValue(type, out alias!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetType(string alias, out Type type) => _aliasTypeDictionary.TryGetValue(alias, out type!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Type> ListTypes() => _aliasTypeDictionary.Values.Distinct();
|
||||
|
||||
private void RegisterTypeName(Type type, string alias)
|
||||
{
|
||||
_aliasTypeDictionary[alias] = type;
|
||||
|
||||
if (type.IsPrimitive || type.IsValueType && Nullable.GetUnderlyingType(type) == null)
|
||||
{
|
||||
var nullableType = typeof(Nullable<>).MakeGenericType(type);
|
||||
_aliasTypeDictionary[$"{alias}?"] = nullableType;
|
||||
|
||||
if (_typeAliasDictionary.ContainsKey(type))
|
||||
_typeAliasDictionary[nullableType] = $"{alias}?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,14 @@ using System.Collections;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Extensions;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Helpers;
|
||||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves workflow JSON type aliases without loading arbitrary CLR type names.
|
||||
/// Resolves serialization type aliases without loading arbitrary CLR type names.
|
||||
/// </summary>
|
||||
public static class WorkflowJsonTypeResolver
|
||||
public static class SerializationTypeResolver
|
||||
{
|
||||
private static readonly IDictionary<string, Type> GenericCollectionTypes = new Dictionary<string, Type>(StringComparer.Ordinal)
|
||||
{
|
||||
|
|
@ -53,50 +52,50 @@ public static class WorkflowJsonTypeResolver
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified workflow JSON type alias.
|
||||
/// Resolves the specified serialization type alias.
|
||||
/// </summary>
|
||||
public static Type ResolveType(IWellKnownTypeRegistry wellKnownTypeRegistry, string? typeAlias)
|
||||
public static Type ResolveType(ISerializationTypeRegistry serializationTypeRegistry, string? typeAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeAlias))
|
||||
throw new JsonException("The workflow JSON type alias is missing.");
|
||||
throw new JsonException("The serialization type alias is missing.");
|
||||
|
||||
if (TryResolveType(wellKnownTypeRegistry, typeAlias, out var type))
|
||||
if (TryResolveType(serializationTypeRegistry, typeAlias, out var type))
|
||||
return type;
|
||||
|
||||
throw new JsonException(
|
||||
$"Unknown workflow JSON type alias '{typeAlias}'. Only registered aliases and supported compound aliases can be deserialized.");
|
||||
$"Unknown serialization type alias '{typeAlias}'. Only registered aliases and supported compound aliases can be deserialized.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve the specified workflow JSON type alias.
|
||||
/// Attempts to resolve the specified serialization type alias.
|
||||
/// </summary>
|
||||
public static bool TryResolveType(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, out Type type)
|
||||
public static bool TryResolveType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, out Type type)
|
||||
{
|
||||
IReadOnlyList<Type>? registeredTypes = null;
|
||||
return TryResolveType(wellKnownTypeRegistry, typeAlias, ref registeredTypes, out type);
|
||||
return TryResolveType(serializationTypeRegistry, typeAlias, ref registeredTypes, out type);
|
||||
}
|
||||
|
||||
private static bool TryResolveType(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
{
|
||||
if (wellKnownTypeRegistry.TryGetType(typeAlias, out var registeredType))
|
||||
if (serializationTypeRegistry.TryGetType(typeAlias, out var registeredType))
|
||||
{
|
||||
type = registeredType;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryResolveArrayType(wellKnownTypeRegistry, typeAlias, ref registeredTypes, out var arrayType))
|
||||
if (TryResolveArrayType(serializationTypeRegistry, typeAlias, ref registeredTypes, out var arrayType))
|
||||
{
|
||||
type = arrayType;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryResolveGenericCollectionType(wellKnownTypeRegistry, typeAlias, ref registeredTypes, out var genericCollectionType))
|
||||
if (TryResolveGenericCollectionType(serializationTypeRegistry, typeAlias, ref registeredTypes, out var genericCollectionType))
|
||||
{
|
||||
type = genericCollectionType;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryResolveRegisteredLegacyTypeName(wellKnownTypeRegistry, typeAlias, ref registeredTypes, out var legacyType))
|
||||
if (TryResolveRegisteredLegacyTypeName(serializationTypeRegistry, typeAlias, ref registeredTypes, out var legacyType))
|
||||
{
|
||||
type = legacyType;
|
||||
return true;
|
||||
|
|
@ -107,18 +106,18 @@ public static class WorkflowJsonTypeResolver
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to return a workflow JSON type alias that this resolver can read back.
|
||||
/// Attempts to return a serialization type alias that this resolver can read back.
|
||||
/// </summary>
|
||||
public static bool TryGetAlias(IWellKnownTypeRegistry wellKnownTypeRegistry, Type type, out string alias)
|
||||
public static bool TryGetAlias(ISerializationTypeRegistry serializationTypeRegistry, Type type, out string alias)
|
||||
{
|
||||
if (wellKnownTypeRegistry.TryGetAlias(type, out alias!))
|
||||
if (serializationTypeRegistry.TryGetAlias(type, out alias!))
|
||||
return true;
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
var elementType = type.GetElementType()!;
|
||||
|
||||
if (TryGetAlias(wellKnownTypeRegistry, elementType, out var elementTypeAlias))
|
||||
if (TryGetAlias(serializationTypeRegistry, elementType, out var elementTypeAlias))
|
||||
{
|
||||
alias = $"{elementTypeAlias}[]";
|
||||
return true;
|
||||
|
|
@ -130,7 +129,7 @@ public static class WorkflowJsonTypeResolver
|
|||
var genericTypeDefinition = type.GetGenericTypeDefinition();
|
||||
|
||||
if (TryGetWritableGenericCollectionAlias(genericTypeDefinition, out var genericTypeAlias) &&
|
||||
TryGetAlias(wellKnownTypeRegistry, type.GenericTypeArguments[0], out var elementTypeAlias))
|
||||
TryGetAlias(serializationTypeRegistry, type.GenericTypeArguments[0], out var elementTypeAlias))
|
||||
{
|
||||
alias = $"{genericTypeAlias}<{elementTypeAlias}>";
|
||||
return true;
|
||||
|
|
@ -163,7 +162,7 @@ public static class WorkflowJsonTypeResolver
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveArrayType(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveArrayType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
{
|
||||
type = null!;
|
||||
|
||||
|
|
@ -171,14 +170,14 @@ public static class WorkflowJsonTypeResolver
|
|||
return false;
|
||||
|
||||
var elementTypeAlias = typeAlias[..^2];
|
||||
if (!TryResolveType(wellKnownTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
return false;
|
||||
|
||||
type = elementType.MakeArrayType();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveGenericCollectionType(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveGenericCollectionType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
{
|
||||
type = null!;
|
||||
var genericStart = typeAlias.IndexOf('<', StringComparison.Ordinal);
|
||||
|
|
@ -191,7 +190,7 @@ public static class WorkflowJsonTypeResolver
|
|||
return false;
|
||||
|
||||
var elementTypeAlias = typeAlias[(genericStart + 1)..^1];
|
||||
if (!TryResolveType(wellKnownTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
return false;
|
||||
|
||||
type = genericTypeDefinition.MakeGenericType(elementType);
|
||||
|
|
@ -213,15 +212,15 @@ public static class WorkflowJsonTypeResolver
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveRegisteredLegacyTypeName(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveRegisteredLegacyTypeName(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
{
|
||||
registeredTypes ??= GetRegisteredTypes(wellKnownTypeRegistry);
|
||||
registeredTypes ??= GetRegisteredTypes(serializationTypeRegistry);
|
||||
var registeredTypeSnapshot = registeredTypes;
|
||||
|
||||
if (TryResolveRegisteredSimpleAssemblyQualifiedName(registeredTypeSnapshot, typeAlias, out type))
|
||||
return true;
|
||||
|
||||
if (TryResolveLegacyGenericCollectionTypeName(wellKnownTypeRegistry, typeAlias, ref registeredTypes, out type))
|
||||
if (TryResolveLegacyGenericCollectionTypeName(serializationTypeRegistry, typeAlias, ref registeredTypes, out type))
|
||||
return true;
|
||||
|
||||
Type? resolvedType;
|
||||
|
|
@ -243,9 +242,9 @@ public static class WorkflowJsonTypeResolver
|
|||
return resolvedType != null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Type> GetRegisteredTypes(IWellKnownTypeRegistry wellKnownTypeRegistry)
|
||||
private static IReadOnlyList<Type> GetRegisteredTypes(ISerializationTypeRegistry serializationTypeRegistry)
|
||||
{
|
||||
return wellKnownTypeRegistry.ListTypes().ToArray();
|
||||
return serializationTypeRegistry.ListTypes().ToArray();
|
||||
}
|
||||
|
||||
private static bool TryResolveRegisteredSimpleAssemblyQualifiedName(IEnumerable<Type> registeredTypes, string typeAlias, out Type type)
|
||||
|
|
@ -257,7 +256,7 @@ public static class WorkflowJsonTypeResolver
|
|||
return type != null;
|
||||
}
|
||||
|
||||
private static bool TryResolveLegacyGenericCollectionTypeName(IWellKnownTypeRegistry wellKnownTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveLegacyGenericCollectionTypeName(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? registeredTypes, out Type type)
|
||||
{
|
||||
type = null!;
|
||||
|
||||
|
|
@ -274,7 +273,7 @@ public static class WorkflowJsonTypeResolver
|
|||
continue;
|
||||
|
||||
var elementTypeAlias = typeAlias[prefix.Length..separatorIndex];
|
||||
if (!TryResolveType(wellKnownTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
|
||||
return false;
|
||||
|
||||
// The resolver only closes known collection definitions over registered element types.
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
using CShells.Features;
|
||||
using Elsa.Common.ShellHandlers;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Elsa.Common.ShellFeatures;
|
||||
|
||||
|
|
@ -9,13 +12,17 @@ namespace Elsa.Common.ShellFeatures;
|
|||
[ShellFeature(
|
||||
"Mediator",
|
||||
DisplayName = "Mediator",
|
||||
Description = "Registers mediator services for in-process notifications and requests")]
|
||||
Description = "Registers mediator services for in-process notifications and requests",
|
||||
DependsOn = [typeof(MultitenancyFeature)])]
|
||||
public class MediatorFeature : IShellFeature
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddMediator()
|
||||
.AddMediatorHostedServices();
|
||||
.AddMediatorBackgroundChannels();
|
||||
|
||||
services.TryAddSingleton<MediatorBackgroundProcessingCoordinator>();
|
||||
services.AddBackgroundTask<MediatorBackgroundTask>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
using Elsa.Mediator.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Common.ShellHandlers;
|
||||
|
||||
public sealed class MediatorBackgroundProcessingCoordinator(
|
||||
BackgroundCommandProcessor commandProcessor,
|
||||
BackgroundNotificationProcessor notificationProcessor,
|
||||
BackgroundJobProcessor jobProcessor,
|
||||
ILogger<MediatorBackgroundProcessingCoordinator> logger) : IAsyncDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _processingTask;
|
||||
private int _referenceCount;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_referenceCount++ > 0)
|
||||
return;
|
||||
|
||||
_cancellationTokenSource = new();
|
||||
_processingTask = Task.WhenAll(
|
||||
commandProcessor.ExecuteAsync(_cancellationTokenSource.Token),
|
||||
notificationProcessor.ExecuteAsync(_cancellationTokenSource.Token),
|
||||
jobProcessor.ExecuteAsync(_cancellationTokenSource.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task? processingTask;
|
||||
CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_referenceCount == 0)
|
||||
return;
|
||||
|
||||
if (--_referenceCount > 0)
|
||||
return;
|
||||
|
||||
processingTask = _processingTask;
|
||||
cancellationTokenSource = _cancellationTokenSource;
|
||||
_processingTask = null;
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
if (cancellationTokenSource is null)
|
||||
return;
|
||||
|
||||
await cancellationTokenSource.CancelAsync();
|
||||
|
||||
if (processingTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await processingTask.WaitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "An error occurred while stopping mediator background processing");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_referenceCount = 1;
|
||||
await StopAsync(CancellationToken.None);
|
||||
_lock.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Common;
|
||||
|
||||
namespace Elsa.Common.ShellHandlers;
|
||||
|
||||
public sealed class MediatorBackgroundTask(MediatorBackgroundProcessingCoordinator coordinator) : BackgroundTask
|
||||
{
|
||||
public override Task StartAsync(CancellationToken cancellationToken) => coordinator.StartAsync(cancellationToken);
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken) => coordinator.StopAsync(cancellationToken);
|
||||
}
|
||||
3
src/modules/Elsa.Diagnostics.ConsoleLogs/AssemblyInfo.cs
Normal file
3
src/modules/Elsa.Diagnostics.ConsoleLogs/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Diagnostics.ConsoleLogs.UnitTests")]
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ConsoleLogStreaming.Core.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Converts the public console stream filter contract values.
|
||||
/// </summary>
|
||||
public sealed class ConsoleStreamJsonConverter : JsonConverter<ConsoleStream?>
|
||||
{
|
||||
public override ConsoleStream? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.String => ReadString(reader.GetString()),
|
||||
JsonTokenType.Number => ReadNumber(ref reader),
|
||||
_ => throw new JsonException("The console log stream filter must be 'stdout', 'stderr', or null.")
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ConsoleStream? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStringValue(value.Value switch
|
||||
{
|
||||
ConsoleStream.Stdout => "stdout",
|
||||
ConsoleStream.Stderr => "stderr",
|
||||
_ => throw new JsonException($"Unsupported console stream value '{value}'.")
|
||||
});
|
||||
}
|
||||
|
||||
private static ConsoleStream? ReadString(string? value)
|
||||
{
|
||||
return value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
null or "" or "all" => null,
|
||||
"stdout" => ConsoleStream.Stdout,
|
||||
"stderr" => ConsoleStream.Stderr,
|
||||
_ => throw new JsonException("The console log stream filter must be 'stdout', 'stderr', or null.")
|
||||
};
|
||||
}
|
||||
|
||||
private static ConsoleStream ReadNumber(ref Utf8JsonReader reader)
|
||||
{
|
||||
if (!reader.TryGetInt32(out var value) || !Enum.IsDefined(typeof(ConsoleStream), value))
|
||||
throw new JsonException("The console log stream filter has an unsupported numeric value.");
|
||||
|
||||
return (ConsoleStream)value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -6,7 +8,9 @@ namespace Elsa.Diagnostics.ConsoleLogs.Contracts;
|
|||
public sealed record ElsaConsoleLogFilter
|
||||
{
|
||||
public string? SourceId { get; init; }
|
||||
public global::ConsoleLogStreaming.Contracts.ConsoleLogStreaming? Stream { get; init; }
|
||||
|
||||
[JsonConverter(typeof(ConsoleStreamJsonConverter))]
|
||||
public global::ConsoleLogStreaming.Core.Models.ConsoleStream? Stream { get; init; }
|
||||
public string? Query { get; init; }
|
||||
public string? WorkflowInstanceId { get; init; }
|
||||
public string? WorkflowDefinitionId { get; init; }
|
||||
|
|
@ -18,4 +22,4 @@ public sealed record ElsaConsoleLogFilter
|
|||
public DateTimeOffset? From { get; init; }
|
||||
public DateTimeOffset? To { get; init; }
|
||||
public int? Limit { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="CShells" />
|
||||
<PackageReference Include="ConsoleLogStreaming.Core" />
|
||||
<PackageReference Include="ConsoleLogStreaming.SignalR" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
using System.Text.Json;
|
||||
using ConsoleLogStreaming.Core;
|
||||
using Elsa.Abstractions;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Permissions;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Endpoints.ConsoleLogs.Recent;
|
||||
|
||||
[PublicAPI]
|
||||
internal class Endpoint(IConsoleLogProvider provider, IConsoleLogStreamingApiMapper mapper) : ElsaEndpoint<ElsaConsoleLogFilter, RecentConsoleLogsResult>
|
||||
internal class Endpoint(IConsoleLogProvider provider) : ElsaEndpointWithoutRequest<RecentConsoleLogsResult>
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Verbs(FastEndpoints.Http.POST);
|
||||
|
|
@ -17,9 +20,47 @@ internal class Endpoint(IConsoleLogProvider provider, IConsoleLogStreamingApiMap
|
|||
ConfigurePermissions(ConsoleLogsPermissions.Read);
|
||||
}
|
||||
|
||||
public override async Task<RecentConsoleLogsResult> ExecuteAsync(ElsaConsoleLogFilter request, CancellationToken cancellationToken)
|
||||
public override async Task<RecentConsoleLogsResult> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await provider.GetRecentAsync(mapper.ToCore(ConsoleLogFilterMapper.ToStreamingFilter(request)), cancellationToken);
|
||||
return mapper.ToApi(result);
|
||||
var request = await ReadJsonBodyAsync(cancellationToken);
|
||||
if (ValidationFailed)
|
||||
{
|
||||
await Send.ErrorsAsync(StatusCodes.Status400BadRequest, cancellationToken);
|
||||
return new();
|
||||
}
|
||||
|
||||
var result = await provider.GetRecentAsync(ConsoleLogFilterMapper.ToStreamingFilter(request ?? new()), cancellationToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<ElsaConsoleLogFilter?> ReadJsonBodyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (HttpContext.Request.ContentLength is 0)
|
||||
return null;
|
||||
|
||||
if (!HttpContext.Request.HasJsonContentType())
|
||||
return null;
|
||||
|
||||
using var body = new MemoryStream();
|
||||
await HttpContext.Request.Body.CopyToAsync(body, cancellationToken);
|
||||
|
||||
if (body.Length == 0)
|
||||
return null;
|
||||
|
||||
body.Position = 0;
|
||||
try
|
||||
{
|
||||
return await DeserializeJsonBodyAsync(body, cancellationToken);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
AddError("Invalid console log filter JSON.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static async ValueTask<ElsaConsoleLogFilter?> DeserializeJsonBodyAsync(Stream body, CancellationToken cancellationToken)
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<ElsaConsoleLogFilter>(body, JsonSerializerOptions, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ using ConsoleLogStreaming.Core;
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Permissions;
|
||||
using JetBrains.Annotations;
|
||||
using ConsoleLogSource = ConsoleLogStreaming.Core.Models.ConsoleLogSource;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Endpoints.ConsoleLogs.Sources;
|
||||
|
||||
[PublicAPI]
|
||||
internal class Endpoint(IConsoleLogProvider provider, IConsoleLogStreamingApiMapper mapper) : ElsaEndpointWithoutRequest<IReadOnlyCollection<ConsoleLogSource>>
|
||||
internal class Endpoint(IConsoleLogProvider provider) : ElsaEndpointWithoutRequest<IReadOnlyCollection<ConsoleLogSource>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -17,6 +18,6 @@ internal class Endpoint(IConsoleLogProvider provider, IConsoleLogStreamingApiMap
|
|||
public override async Task<IReadOnlyCollection<ConsoleLogSource>> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await provider.ListSourcesAsync(cancellationToken);
|
||||
return sources.Select(mapper.ToApi).ToList();
|
||||
return sources.ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
|
|
@ -13,7 +10,7 @@ internal static class ConsoleLogContextServiceCollectionExtensions
|
|||
{
|
||||
services.TryAddSingleton(_ => ConsoleLogContextAccessor.Instance);
|
||||
services.TryAddSingleton<IConsoleLogContextAccessor>(sp => sp.GetRequiredService<ConsoleLogContextAccessor>());
|
||||
services.TryAddSingleton<IConsoleLogMetadataAccessor>(sp => sp.GetRequiredService<ConsoleLogContextAccessor>());
|
||||
services.TryAddSingleton<ConsoleLogStreaming.Core.IConsoleLogMetadataAccessor>(sp => sp.GetRequiredService<ConsoleLogContextAccessor>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Scoped<IWorkflowExecutionPipelineContributor, ConsoleLogWorkflowExecutionPipelineContributor>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Scoped<IActivityExecutionPipelineContributor, ConsoleLogActivityExecutionPipelineContributor>());
|
||||
return services;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Extensions;
|
||||
|
||||
internal static class ConsoleLogProviderServiceCollectionExtensions
|
||||
{
|
||||
public static void DecorateConsoleLogProvider(this IServiceCollection services)
|
||||
{
|
||||
var descriptor = services.LastOrDefault(x => x.ServiceType == typeof(IConsoleLogProvider));
|
||||
if (descriptor == null)
|
||||
return;
|
||||
|
||||
services.TryAddSingleton<ElsaConsoleLogRecentBuffer>();
|
||||
services.Remove(descriptor);
|
||||
services.Add(ServiceDescriptor.Describe(
|
||||
typeof(IConsoleLogProvider),
|
||||
sp => new ElsaConsoleLogProvider(
|
||||
CreateProvider(sp, descriptor),
|
||||
sp.GetRequiredService<IConsoleLogContextAccessor>(),
|
||||
sp.GetRequiredService<ElsaConsoleLogRecentBuffer>()),
|
||||
descriptor.Lifetime));
|
||||
}
|
||||
|
||||
private static IConsoleLogProvider CreateProvider(IServiceProvider serviceProvider, ServiceDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ImplementationInstance is IConsoleLogProvider instance)
|
||||
return instance;
|
||||
|
||||
if (descriptor.ImplementationFactory != null)
|
||||
return (IConsoleLogProvider)descriptor.ImplementationFactory(serviceProvider)!;
|
||||
|
||||
if (descriptor.ImplementationType != null)
|
||||
return (IConsoleLogProvider)ActivatorUtilities.CreateInstance(serviceProvider, descriptor.ImplementationType);
|
||||
|
||||
throw new InvalidOperationException("The console log provider registration is invalid.");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
using ConsoleLogStreaming.SignalR;
|
||||
using Elsa.Diagnostics.ConsoleLogs.RealTime;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Extensions;
|
||||
|
||||
|
|
@ -13,12 +10,6 @@ public static class EndpointRouteBuilderExtensions
|
|||
|
||||
public static void MapConsoleLogsHub(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<ConsoleLogStreamingSignalROptions>>().Value;
|
||||
var hub = endpoints.MapHub<ElsaConsoleLogsHub>(options.HubPath);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.AuthorizationPolicy))
|
||||
hub.RequireAuthorization(options.AuthorizationPolicy);
|
||||
|
||||
options.ConfigureHubEndpoint?.Invoke(hub);
|
||||
endpoints.MapHub<ElsaConsoleLogsHub>(HubRoute);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
using ConsoleLogStreaming.Contracts;
|
||||
using ConsoleLogStreaming.Core;
|
||||
using ConsoleLogStreaming.Core.DependencyInjection;
|
||||
using ConsoleLogStreaming.Core.Options;
|
||||
using ConsoleLogStreaming.SignalR;
|
||||
using ConsoleLogStreaming.SignalR.DependencyInjection;
|
||||
using Elsa.Diagnostics.ConsoleLogs.RealTime;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Extensions;
|
||||
|
||||
|
|
@ -19,14 +16,16 @@ public static class HostServiceCollectionExtensions
|
|||
public static IServiceCollection AddConsoleLogsHost(this IServiceCollection services, Action<ConsoleLogOptions>? configure = null)
|
||||
{
|
||||
services.AddConsoleLogContextServices();
|
||||
services.AddConsoleLogStreamingHost(options =>
|
||||
services.AddConsoleLogStreaming(options =>
|
||||
{
|
||||
ElsaConsoleLogOptions.ConfigureDefaults(options);
|
||||
configure?.Invoke(options);
|
||||
});
|
||||
services.AddConsoleLogStreamingSignalR(options => options.HubPath = EndpointRouteBuilderExtensions.HubRoute);
|
||||
services.TryAddSingleton<IConsoleLogStreamingHubAuthorizer, ElsaConsoleLogStreamingHubAuthorizer>();
|
||||
services.AddSignalR();
|
||||
services.DecorateConsoleLogProvider();
|
||||
services.TryAddSingleton<IElsaConsoleLogHubAuthorizer, ElsaConsoleLogStreamHubAuthorizer>();
|
||||
services.TryAddSingleton<ElsaConsoleLogSubscriptionManager>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, ConsoleLogCaptureHostedService>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
using ConsoleLogStreaming.Contracts;
|
||||
using ConsoleLogStreaming.Core;
|
||||
using ConsoleLogStreaming.Core.DependencyInjection;
|
||||
using ConsoleLogStreaming.Core.Options;
|
||||
using ConsoleLogStreaming.SignalR;
|
||||
using ConsoleLogStreaming.SignalR.DependencyInjection;
|
||||
using CShells.Lifecycle;
|
||||
using Elsa.Diagnostics.ConsoleLogs.RealTime;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -19,14 +16,18 @@ public static class ServiceCollectionExtensions
|
|||
public static IServiceCollection AddConsoleLogsServices(this IServiceCollection services, Action<ConsoleLogOptions>? configureOptions = null)
|
||||
{
|
||||
services.AddConsoleLogContextServices();
|
||||
services.AddConsoleLogStreamingHost(options =>
|
||||
services.AddConsoleLogStreaming(options =>
|
||||
{
|
||||
ElsaConsoleLogOptions.ConfigureDefaults(options);
|
||||
configureOptions?.Invoke(options);
|
||||
});
|
||||
services.AddConsoleLogStreamingSignalR(options => options.HubPath = EndpointRouteBuilderExtensions.HubRoute);
|
||||
services.TryAddSingleton<IConsoleLogStreamingHubAuthorizer, ElsaConsoleLogStreamingHubAuthorizer>();
|
||||
services.AddSignalR();
|
||||
services.DecorateConsoleLogProvider();
|
||||
services.TryAddSingleton<IElsaConsoleLogHubAuthorizer, ElsaConsoleLogStreamHubAuthorizer>();
|
||||
services.TryAddSingleton<ElsaConsoleLogSubscriptionManager>();
|
||||
services.TryAddScoped<ConsoleLogCaptureShellLease>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Scoped<IShellInitializer, ConsoleLogCaptureShellInitializer>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Scoped<IDrainHandler, ConsoleLogCaptureShellDrainHandler>());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ tee, the capture publishes them verbatim, and the Studio renders them.
|
|||
|
||||
Recent, source, and hub access all require the same permission.
|
||||
|
||||
Custom storage providers should implement `ConsoleLogStreaming.Core.IConsoleLogProvider` and use the core models. Elsa-specific values such as workflow instance IDs are represented as metadata internally and projected onto shared `ConsoleLogStreaming.Contracts` DTOs at the REST and SignalR boundaries.
|
||||
Custom storage providers should implement `ConsoleLogStreaming.Core.IConsoleLogProvider` and use the core models. Elsa-specific values such as workflow instance IDs are represented as metadata internally and projected onto shared `ConsoleLogStreaming.Core.Models` DTOs at the REST and SignalR boundaries.
|
||||
|
||||
## Safety Boundaries
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
using System.Collections.Concurrent;
|
||||
using ConsoleLogStreaming.Contracts;
|
||||
using ConsoleLogStreaming.Core;
|
||||
using ConsoleLogStreaming.SignalR;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CoreConsoleLogSource = ConsoleLogStreaming.Core.Models.ConsoleLogSource;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.RealTime;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -18,8 +13,7 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
private readonly ConcurrentDictionary<string, ConsoleLogSubscription> _subscriptions = new(StringComparer.Ordinal);
|
||||
private readonly IConsoleLogProvider _provider;
|
||||
private readonly IConsoleLogSourceRegistry _sourceRegistry;
|
||||
private readonly IConsoleLogStreamingApiMapper _mapper;
|
||||
private readonly IHubContext<ElsaConsoleLogsHub, IConsoleLogsClient> _hubContext;
|
||||
private readonly IHubContext<ElsaConsoleLogsHub, IElsaConsoleLogsClient> _hubContext;
|
||||
private readonly ILogger<ElsaConsoleLogSubscriptionManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -28,13 +22,11 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
public ElsaConsoleLogSubscriptionManager(
|
||||
IConsoleLogProvider provider,
|
||||
IConsoleLogSourceRegistry sourceRegistry,
|
||||
IConsoleLogStreamingApiMapper mapper,
|
||||
IHubContext<ElsaConsoleLogsHub, IConsoleLogsClient> hubContext,
|
||||
IHubContext<ElsaConsoleLogsHub, IElsaConsoleLogsClient> hubContext,
|
||||
ILogger<ElsaConsoleLogSubscriptionManager> logger)
|
||||
{
|
||||
_provider = provider;
|
||||
_sourceRegistry = sourceRegistry;
|
||||
_mapper = mapper;
|
||||
_hubContext = hubContext;
|
||||
_logger = logger;
|
||||
_sourceRegistry.SourceChanged += OnSourceChanged;
|
||||
|
|
@ -89,13 +81,24 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
{
|
||||
try
|
||||
{
|
||||
await foreach (var item in _provider.SubscribeAsync(_mapper.ToCore(ConsoleLogFilterMapper.ToStreamingFilter(filter)), cancellationToken).ConfigureAwait(false))
|
||||
string? lastSourceSignature = null;
|
||||
|
||||
await foreach (var item in _provider.SubscribeAsync(ConsoleLogFilterMapper.ToStreamingFilter(filter), cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (item.Line != null)
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveConsoleLogLineAsync(_mapper.ToApi(item.Line), cancellationToken).ConfigureAwait(false);
|
||||
{
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveConsoleLogLineAsync(item.Line, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var sourceSignature = GetSourceSignature(item.Line.Source);
|
||||
if (!string.Equals(sourceSignature, lastSourceSignature, StringComparison.Ordinal))
|
||||
{
|
||||
lastSourceSignature = sourceSignature;
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveSourceChangedAsync(item.Line.Source, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Dropped != null)
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveDroppedLinesAsync(_mapper.ToApi(item.Dropped), cancellationToken).ConfigureAwait(false);
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveDroppedLinesAsync(item.Dropped, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
|
|
@ -128,12 +131,12 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
subscription.CancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
private void OnSourceChanged(CoreConsoleLogSource source)
|
||||
private void OnSourceChanged(ConsoleLogSource source)
|
||||
{
|
||||
_ = BroadcastSourceChangedAsync(source, _subscriptions.ToArray());
|
||||
}
|
||||
|
||||
private async Task BroadcastSourceChangedAsync(CoreConsoleLogSource source, IReadOnlyCollection<KeyValuePair<string, ConsoleLogSubscription>> subscriptions)
|
||||
private async Task BroadcastSourceChangedAsync(ConsoleLogSource source, IReadOnlyCollection<KeyValuePair<string, ConsoleLogSubscription>> subscriptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -142,7 +145,7 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
if (!MatchesSource(source, subscription.Filter))
|
||||
continue;
|
||||
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveSourceChangedAsync(_mapper.ToApi(source), subscription.CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await _hubContext.Clients.Client(connectionId).ReceiveSourceChangedAsync(source, subscription.CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
|
|
@ -155,10 +158,27 @@ public sealed class ElsaConsoleLogSubscriptionManager : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static bool MatchesSource(CoreConsoleLogSource source, ElsaConsoleLogFilter filter)
|
||||
private static bool MatchesSource(ConsoleLogSource source, ElsaConsoleLogFilter filter)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(filter.SourceId) || string.Equals(source.Id, filter.SourceId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetSourceSignature(ConsoleLogSource source)
|
||||
{
|
||||
var metadata = source.Metadata
|
||||
.OrderBy(x => x.Key, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.Value, StringComparer.Ordinal)
|
||||
.Select(x => $"{x.Key}={x.Value}");
|
||||
|
||||
return string.Join('\u001f',
|
||||
source.Id,
|
||||
source.DisplayName,
|
||||
source.ServiceName,
|
||||
source.ProcessId?.ToString(),
|
||||
source.MachineName,
|
||||
source.Health.ToString(),
|
||||
string.Join('\u001e', metadata));
|
||||
}
|
||||
|
||||
private sealed record ConsoleLogSubscription(ElsaConsoleLogFilter Filter, CancellationTokenSource CancellationTokenSource);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using ConsoleLogStreaming.Contracts;
|
||||
using ConsoleLogStreaming.Core;
|
||||
using ConsoleLogStreaming.SignalR;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.RealTime;
|
||||
|
|
@ -11,11 +8,11 @@ namespace Elsa.Diagnostics.ConsoleLogs.RealTime;
|
|||
/// <summary>
|
||||
/// SignalR hub for live Elsa console log streaming.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public sealed class ElsaConsoleLogsHub(
|
||||
IConsoleLogProvider provider,
|
||||
IConsoleLogStreamingApiMapper mapper,
|
||||
IConsoleLogStreamingHubAuthorizer authorizer,
|
||||
ElsaConsoleLogSubscriptionManager subscriptionManager) : Hub<IConsoleLogsClient>
|
||||
IElsaConsoleLogHubAuthorizer authorizer,
|
||||
ElsaConsoleLogSubscriptionManager subscriptionManager) : Hub<IElsaConsoleLogsClient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams matching console log items as a SignalR streaming method.
|
||||
|
|
@ -27,8 +24,8 @@ public sealed class ElsaConsoleLogsHub(
|
|||
await EnsureCanReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
filter = ValidateFilter(filter);
|
||||
|
||||
await foreach (var item in provider.SubscribeAsync(mapper.ToCore(ConsoleLogFilterMapper.ToStreamingFilter(filter)), cancellationToken).ConfigureAwait(false))
|
||||
yield return mapper.ToApi(item);
|
||||
await foreach (var item in provider.SubscribeAsync(ConsoleLogFilterMapper.ToStreamingFilter(filter), cancellationToken).ConfigureAwait(false))
|
||||
yield return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -80,3 +77,10 @@ public sealed class ElsaConsoleLogsHub(
|
|||
throw new HubException("Access denied.");
|
||||
}
|
||||
}
|
||||
|
||||
public interface IElsaConsoleLogsClient
|
||||
{
|
||||
Task ReceiveConsoleLogLineAsync(ConsoleLogLine line, CancellationToken cancellationToken = default);
|
||||
Task ReceiveDroppedLinesAsync(ConsoleLogDroppedSummary summary, CancellationToken cancellationToken = default);
|
||||
Task ReceiveSourceChangedAsync(ConsoleLogSource source, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
internal sealed class ConsoleLogCaptureHostedService(IConsoleLogCapture capture) : IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken) => capture.StartAsync(cancellationToken).AsTask();
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => capture.StopAsync(cancellationToken).AsTask();
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
using ConsoleLogStreaming.Core.Capture;
|
||||
using CShells.Lifecycle;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
public class ConsoleLogCaptureShellLease(IServiceProvider serviceProvider)
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private bool _started;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_started)
|
||||
return;
|
||||
|
||||
ConsoleLogStreamingHost.AddReference();
|
||||
var releaseReference = true;
|
||||
|
||||
try
|
||||
{
|
||||
var capture = serviceProvider.GetRequiredService<IConsoleLogCapture>();
|
||||
await capture.StartAsync(cancellationToken);
|
||||
_started = true;
|
||||
releaseReference = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (releaseReference)
|
||||
await ConsoleLogStreamingHost.ReleaseReferenceAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (!_started)
|
||||
return;
|
||||
|
||||
_started = false;
|
||||
await ConsoleLogStreamingHost.ReleaseReferenceAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleLogCaptureShellInitializer(ConsoleLogCaptureShellLease lease) : IShellInitializer
|
||||
{
|
||||
public Task InitializeAsync(CancellationToken cancellationToken = default) => lease.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public class ConsoleLogCaptureShellDrainHandler(ConsoleLogCaptureShellLease lease) : IDrainHandler
|
||||
{
|
||||
public Task DrainAsync(IDrainExtensionHandle extensionHandle, CancellationToken cancellationToken) => lease.StopAsync(cancellationToken);
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Ambient console log context backed by the current async execution context.
|
||||
/// </summary>
|
||||
public sealed class ConsoleLogContextAccessor : IConsoleLogContextAccessor, IConsoleLogMetadataAccessor
|
||||
public sealed class ConsoleLogContextAccessor : IConsoleLogContextAccessor, ConsoleLogStreaming.Core.IConsoleLogMetadataAccessor
|
||||
{
|
||||
private static readonly AsyncLocal<MetadataFrame?> CurrentFrame = new();
|
||||
private static readonly IReadOnlyDictionary<string, string> Empty = new Dictionary<string, string>();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
internal static class ConsoleLogContextAccessorExtensions
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
using ConsoleLogStreaming.Contracts;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
internal static class ConsoleLogFilterMapper
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ internal static class ElsaConsoleLogOptions
|
|||
options.SourceDisplayName = !string.IsNullOrWhiteSpace(podName) ? podName : sourceId;
|
||||
options.ServiceName = Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME") ?? AppDomain.CurrentDomain.FriendlyName;
|
||||
options.PreserveAnsi = true;
|
||||
options.RecentCapacity = 2_000;
|
||||
options.MaxRecentQuerySize = 250;
|
||||
|
||||
SetMetadata(options, ConsoleLogStreamingApiMetadataKeys.KubernetesPodName, podName);
|
||||
SetMetadata(options, ConsoleLogStreamingApiMetadataKeys.KubernetesNamespace, Environment.GetEnvironmentVariable("POD_NAMESPACE"));
|
||||
SetMetadata(options, ConsoleLogStreamingApiMetadataKeys.ContainerName, Environment.GetEnvironmentVariable("CONTAINER_NAME"));
|
||||
SetMetadata(options, ConsoleLogStreamingApiMetadataKeys.KubernetesNodeName, Environment.GetEnvironmentVariable("NODE_NAME"));
|
||||
SetMetadata(options, ConsoleLogStreamingApiMetadataKeys.ProcessStartedAt, DateTimeOffset.UtcNow.ToString("O"));
|
||||
SetMetadata(options, "kubernetes.pod.name", podName);
|
||||
SetMetadata(options, "kubernetes.namespace.name", Environment.GetEnvironmentVariable("POD_NAMESPACE"));
|
||||
SetMetadata(options, "container.name", Environment.GetEnvironmentVariable("CONTAINER_NAME"));
|
||||
SetMetadata(options, "kubernetes.node.name", Environment.GetEnvironmentVariable("NODE_NAME"));
|
||||
SetMetadata(options, "process.started_at", DateTimeOffset.UtcNow.ToString("O"));
|
||||
}
|
||||
|
||||
private static void SetMetadata(ConsoleLogOptions options, string key, string? value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
using ConsoleLogStreaming.Core;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
internal sealed class ElsaConsoleLogProvider(
|
||||
IConsoleLogProvider inner,
|
||||
IConsoleLogContextAccessor contextAccessor,
|
||||
ElsaConsoleLogRecentBuffer recentBuffer) : IConsoleLogProvider, IDisposable, IAsyncDisposable
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyMetadataFilter = new Dictionary<string, string>();
|
||||
|
||||
public ValueTask PublishAsync(ConsoleLogLine line, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var enrichedLine = Enrich(line);
|
||||
recentBuffer.Add(enrichedLine);
|
||||
return inner.PublishAsync(enrichedLine, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask<RecentConsoleLogsResult> GetRecentAsync(ConsoleLogFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!HasMetadataFilter(filter))
|
||||
return await inner.GetRecentAsync(filter, cancellationToken);
|
||||
|
||||
var result = await inner.GetRecentAsync(filter with
|
||||
{
|
||||
Metadata = EmptyMetadataFilter
|
||||
}, cancellationToken);
|
||||
var items = recentBuffer.Query(filter);
|
||||
|
||||
return result with { Items = items };
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ConsoleLogStreamingItem> SubscribeAsync(
|
||||
ConsoleLogFilter filter,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!HasMetadataFilter(filter))
|
||||
{
|
||||
await foreach (var item in inner.SubscribeAsync(filter, cancellationToken).ConfigureAwait(false))
|
||||
yield return item;
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
await foreach (var item in inner.SubscribeAsync(filter with { Metadata = EmptyMetadataFilter }, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (item.Line != null && !MatchesMetadata(item.Line, filter.Metadata))
|
||||
continue;
|
||||
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyCollection<ConsoleLogSource>> ListSourcesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return inner.ListSourcesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
switch (inner)
|
||||
{
|
||||
case IDisposable disposable:
|
||||
disposable.Dispose();
|
||||
break;
|
||||
case IAsyncDisposable asyncDisposable:
|
||||
asyncDisposable.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
switch (inner)
|
||||
{
|
||||
case IAsyncDisposable asyncDisposable:
|
||||
await asyncDisposable.DisposeAsync();
|
||||
break;
|
||||
case IDisposable disposable:
|
||||
disposable.Dispose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private ConsoleLogLine Enrich(ConsoleLogLine line)
|
||||
{
|
||||
var ambientMetadata = contextAccessor.GetMetadata();
|
||||
if (ambientMetadata.Count == 0)
|
||||
return line;
|
||||
|
||||
var metadata = new Dictionary<string, string>(line.Metadata, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in ambientMetadata)
|
||||
metadata[item.Key] = item.Value;
|
||||
|
||||
return line with { Metadata = metadata };
|
||||
}
|
||||
|
||||
private static bool HasMetadataFilter(ConsoleLogFilter filter) => filter.Metadata.Count > 0;
|
||||
|
||||
private static bool MatchesMetadata(ConsoleLogLine line, IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
foreach (var (key, value) in metadata)
|
||||
{
|
||||
if (!line.Metadata.TryGetValue(key, out var candidate) || !string.Equals(candidate, value, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using ConsoleLogStreaming.Core.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
internal sealed class ElsaConsoleLogRecentBuffer
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly Queue<ConsoleLogLine> _lines = new();
|
||||
private readonly int _capacity;
|
||||
private readonly int _maxQuerySize;
|
||||
|
||||
public ElsaConsoleLogRecentBuffer(IOptions<ConsoleLogOptions> options)
|
||||
{
|
||||
_capacity = Math.Max(1, options.Value.RecentCapacity);
|
||||
_maxQuerySize = Math.Max(1, options.Value.MaxRecentQuerySize);
|
||||
}
|
||||
|
||||
public void Add(ConsoleLogLine line)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lines.Enqueue(line);
|
||||
|
||||
while (_lines.Count > _capacity)
|
||||
_lines.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ConsoleLogLine> Query(ConsoleLogFilter filter)
|
||||
{
|
||||
ConsoleLogLine[] snapshot;
|
||||
|
||||
lock (_lock)
|
||||
snapshot = _lines.ToArray();
|
||||
|
||||
var limit = filter.Limit is > 0 ? Math.Min(filter.Limit.Value, _maxQuerySize) : _maxQuerySize;
|
||||
return snapshot
|
||||
.Where(line => Matches(line, filter))
|
||||
.TakeLast(limit)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool Matches(ConsoleLogLine line, ConsoleLogFilter filter)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(filter.SourceId) && !string.Equals(line.Source.Id, filter.SourceId, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (filter.Stream.HasValue && line.Stream != filter.Stream.Value)
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Query) && !line.Text.Contains(filter.Query, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (filter.From.HasValue && line.ReceivedAt < filter.From.Value)
|
||||
return false;
|
||||
|
||||
if (filter.To.HasValue && line.ReceivedAt > filter.To.Value)
|
||||
return false;
|
||||
|
||||
foreach (var (key, value) in filter.Metadata)
|
||||
{
|
||||
if (!line.Metadata.TryGetValue(key, out var candidate) || !string.Equals(candidate, value, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
using ConsoleLogStreaming.SignalR;
|
||||
using Elsa.Diagnostics.ConsoleLogs.Permissions;
|
||||
using FastEndpoints.Security;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Elsa.Diagnostics.ConsoleLogs.Services;
|
||||
|
||||
public sealed class ElsaConsoleLogStreamingHubAuthorizer : IConsoleLogStreamingHubAuthorizer
|
||||
public interface IElsaConsoleLogHubAuthorizer
|
||||
{
|
||||
ValueTask<bool> CanReadAsync(HubCallerContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class ElsaConsoleLogStreamHubAuthorizer : IElsaConsoleLogHubAuthorizer
|
||||
{
|
||||
private const string ReadAllPermission = "read:*";
|
||||
private static readonly string[] ReadPermissions = [PermissionNames.All, ReadAllPermission, ConsoleLogsPermissions.Read];
|
||||
|
|
|
|||
|
|
@ -24,14 +24,11 @@ public class ConsoleLogsFeature : IFastEndpointsShellFeature, IWebShellFeature
|
|||
|
||||
public int RecentLogCapacity { get; set; } = DefaultOptions.RecentCapacity;
|
||||
public int SubscriberChannelCapacity { get; set; } = DefaultOptions.SubscriberCapacity;
|
||||
public int CaptureChannelCapacity { get; set; } = DefaultOptions.CaptureChannelCapacity;
|
||||
public int MaxRecentQuerySize { get; set; } = DefaultOptions.MaxRecentQuerySize;
|
||||
public int MaxLineLength { get; set; } = DefaultOptions.MaxLineLength;
|
||||
public TimeSpan IdleFlushTimeout { get; set; } = DefaultOptions.IdleFlushTimeout;
|
||||
public bool StripAnsiEscapeSequences { get; set; } = !DefaultOptions.PreserveAnsi;
|
||||
public TimeSpan SourceHeartbeatTimeout { get; set; } = DefaultOptions.SourceHeartbeatTimeout;
|
||||
public string RedactionReplacement { get; set; } = DefaultOptions.RedactionReplacement;
|
||||
public ICollection<string> SensitiveNames { get; set; } = [..DefaultOptions.SensitiveNames];
|
||||
public string RedactionReplacement { get; set; } = "[Redacted]";
|
||||
public ICollection<string> SensitiveTextPatterns { get; set; } = [..DefaultOptions.RedactionRules.Select(x => x.Pattern)];
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
|
|
@ -48,14 +45,10 @@ public class ConsoleLogsFeature : IFastEndpointsShellFeature, IWebShellFeature
|
|||
{
|
||||
options.RecentCapacity = RecentLogCapacity;
|
||||
options.SubscriberCapacity = SubscriberChannelCapacity;
|
||||
options.CaptureChannelCapacity = CaptureChannelCapacity;
|
||||
options.MaxRecentQuerySize = MaxRecentQuerySize;
|
||||
options.MaxLineLength = MaxLineLength;
|
||||
options.IdleFlushTimeout = IdleFlushTimeout;
|
||||
options.PreserveAnsi = !StripAnsiEscapeSequences;
|
||||
options.SourceHeartbeatTimeout = SourceHeartbeatTimeout;
|
||||
options.RedactionReplacement = RedactionReplacement;
|
||||
options.SensitiveNames = [..SensitiveNames];
|
||||
options.RedactionRules.Clear();
|
||||
foreach (var pattern in SensitiveTextPatterns)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
global using System.Runtime.CompilerServices;
|
||||
global using ConsoleLogStreaming.Contracts;
|
||||
global using ConsoleLogStreaming.Core.Models;
|
||||
global using Elsa.Diagnostics.ConsoleLogs.Contracts;
|
||||
global using Elsa.Workflows;
|
||||
global using Elsa.Workflows.Pipelines.ActivityExecution;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Diagnostics.OpenTelemetry.UnitTests")]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface ICollectorConfigurationProvider
|
||||
{
|
||||
ValueTask<CollectorConfiguration> GetAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetryIngestor
|
||||
{
|
||||
ValueTask IngestAsync(OpenTelemetryBatch batch, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetryLiveFeed
|
||||
{
|
||||
ValueTask PublishAsync(OpenTelemetryBatch batch, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<OpenTelemetryStreamItem> SubscribeAsync(OpenTelemetryTraceFilter filter, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetryProvider
|
||||
{
|
||||
ValueTask<OpenTelemetryResourceResult> GetResourcesAsync(OpenTelemetryResourceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryTraceResult> GetTracesAsync(OpenTelemetryTraceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryTraceDetail?> GetTraceAsync(string traceId, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryMetricResult> GetMetricsAsync(OpenTelemetryMetricFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryLogResult> GetLogsAsync(OpenTelemetryLogFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryStorageDiagnostics> GetStorageDiagnosticsAsync(CancellationToken cancellationToken = default);
|
||||
ValueTask<CollectorConfiguration> GetCollectorConfigurationAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetryRedactor
|
||||
{
|
||||
OpenTelemetryBatch Redact(OpenTelemetryBatch batch);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetrySourceRegistry
|
||||
{
|
||||
long DroppedCount { get; }
|
||||
void MarkSeen(TelemetryResource resource);
|
||||
IReadOnlyCollection<TelemetryResource> List();
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Diagnostics.OpenTelemetry.Models;
|
||||
|
||||
namespace Elsa.Diagnostics.OpenTelemetry.Contracts;
|
||||
|
||||
public interface IOpenTelemetryStore
|
||||
{
|
||||
ValueTask WriteAsync(OpenTelemetryBatch batch, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryResourceResult> QueryResourcesAsync(OpenTelemetryResourceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryTraceResult> QueryTracesAsync(OpenTelemetryTraceFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryTraceDetail?> GetTraceAsync(string traceId, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryMetricResult> QueryMetricsAsync(OpenTelemetryMetricFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryLogResult> QueryLogsAsync(OpenTelemetryLogFilter filter, CancellationToken cancellationToken = default);
|
||||
ValueTask<OpenTelemetryStorageDiagnostics> GetDiagnosticsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Provides OpenTelemetry diagnostics collection and query services for Elsa hosts.</Description>
|
||||
<PackageTags>elsa module diagnostics opentelemetry otel otlp signalr operations</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue