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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ The environment layer is organised into three planes:
| **Application** | Solutions, packages, deployments, schema management | `txc env sln …`, `txc env pkg …`, `txc env deploy …`, `txc env entity …` |
| **Data** | Records, queries, bulk operations, CMT import/export | `txc env data …`, `txc data …` |

Read-only **diagnostics** cut across all three — read runtime logs straight from the terminal with `txc env log …` (plug-in traces, async jobs, workflow runs). See [Diagnostics — Environment Logs](#diagnostics--environment-logs).

### Workspace — Local-First Development

The fastest way to build Dataverse components. Everything happens locally in your repo — no environment round-trips, no publish waits. Ideal for coding agents that need to scaffold dozens of components in a session.
Expand Down Expand Up @@ -171,6 +173,38 @@ txc env component layer list --entity account --attribute revenue
txc env component dep delete-check --entity tom_project
```

### Diagnostics — Environment Logs

Read runtime logs straight from the terminal instead of dropping into raw SQL or the maker portal.

```sh
# Unified recent feed across plug-in traces and async jobs
txc env log list --since 24h

# Plug-in execution traces (needs plug-in trace logging enabled in the environment)
txc env log plugin-trace --since 1h --errors-only
txc env log plugin-trace --plugin Acme.Plugins.Account --entity account

# Background system jobs and classic workflow runs
txc env log async-jobs --errors-only
txc env log workflow --since 7d

# Live tail — keep watching new async jobs as they appear (Ctrl+C to stop)
txc env log async-jobs --follow
txc env log async-jobs --follow --errors-only --interval 10

# Follow one operation across sources by its correlation id
txc env log list --correlation-id 5f9c2e7a-1234-4abc-9def-0123456789ab
```

Shared flags: `--since` (`30m`/`24h`/`7d`/`2w`), `--entity`, `--errors-only`, `--correlation-id`, `--top`
(`--plugin` applies to `plugin-trace` and `list`). Add `--format json` for machine-readable output.

> [!NOTE]
> `plugin-trace` is empty unless plug-in trace logging is enabled in the environment
> (System Settings → Customization → plug-in trace log). Async-job rows are purged by Dataverse
> retention after a few days, so very old runs may no longer be present.

### Data Plane

Query, create, update, and bulk-operate on Dataverse records.
Expand Down
111 changes: 111 additions & 0 deletions src/TALXIS.CLI.Core/Contracts/Dataverse/IEnvironmentLogService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
namespace TALXIS.CLI.Core.Contracts.Dataverse;

/// <summary>
/// Parsed, source-agnostic filter applied to environment-log reads. Built once
/// by a leaf command from its CLI flags and passed straight to the reader so the
/// service signatures stay small.
/// </summary>
/// <param name="SinceUtc">Lower bound on the row timestamp (<c>createdon</c>). <c>null</c> = no bound.</param>
/// <param name="Entity">Logical name of an entity to filter by (plug-in <c>primaryentity</c> / async regarding object). <c>null</c> = all.</param>
/// <param name="Plugin">Plug-in / activity type-name substring to filter by (plug-in traces only). <c>null</c> = all.</param>
/// <param name="ErrorsOnly">When <c>true</c>, keep only error rows (traces with an exception, failed/cancelled jobs).</param>
/// <param name="CorrelationId">Restrict to a single operation's correlation id. <c>null</c> = all.</param>
/// <param name="Top">Maximum number of rows to return.</param>
public sealed record EnvironmentLogFilter(
DateTime? SinceUtc,
string? Entity,
string? Plugin,
bool ErrorsOnly,
Guid? CorrelationId,
int Top);

/// <summary>
/// Row from the Dataverse <c>plugintracelog</c> table. All <see cref="DateTime"/>
/// values are UTC.
/// </summary>
public sealed record PluginTraceRecord(
Guid Id,
DateTime? CreatedOnUtc,
string? TypeName,
string? MessageName,
string? PrimaryEntity,
string? Mode,
int? Depth,
long? DurationMs,
bool HasException,
string? ExceptionSnippet,
string? MessageSnippet,
Guid? CorrelationId);

/// <summary>
/// Row from the Dataverse <c>asyncoperation</c> table (background jobs, including
/// classic workflow runs). All <see cref="DateTime"/> values are UTC.
/// </summary>
public sealed record AsyncJobRecord(
Guid Id,
string? Name,
int? OperationTypeCode,
string? OperationTypeLabel,
string? StatusLabel,
bool IsError,
DateTime? CreatedOnUtc,
DateTime? StartedOnUtc,
DateTime? CompletedOnUtc,
string? RegardingEntity,
string? Message,
Guid? CorrelationId);

/// <summary>
/// Reads runtime diagnostic logs from a live environment — plug-in traces and
/// async jobs (background workflows included). Hides the underlying readers and
/// connection lifetime from feature commands.
/// </summary>
public interface IEnvironmentLogService
{
/// <summary>
/// Reads recent <c>plugintracelog</c> rows ordered by <c>createdon</c> descending,
/// honouring <paramref name="filter"/>.
/// </summary>
Task<IReadOnlyList<PluginTraceRecord>> GetPluginTracesAsync(
string? profileName,
EnvironmentLogFilter filter,
CancellationToken ct);

/// <summary>
/// Reads recent <c>asyncoperation</c> rows of any operation type, ordered by
/// <c>createdon</c> descending, honouring <paramref name="filter"/>.
/// </summary>
Task<IReadOnlyList<AsyncJobRecord>> GetAsyncJobsAsync(
string? profileName,
EnvironmentLogFilter filter,
CancellationToken ct);

/// <summary>
/// Reads recent classic (background) workflow runs — <c>asyncoperation</c> rows
/// whose operation type is Workflow — ordered by <c>createdon</c> descending,
/// honouring <paramref name="filter"/>.
/// </summary>
Task<IReadOnlyList<AsyncJobRecord>> GetWorkflowRunsAsync(
string? profileName,
EnvironmentLogFilter filter,
CancellationToken ct);

/// <summary>
/// Resolves the profile and opens the environment connection once, returning a
/// reader bound to that connection. Use this for a <c>--follow</c> poll loop so
/// each tick doesn't re-resolve the profile and re-open a connection.
/// </summary>
Task<IAsyncJobLogReader> CreateAsyncJobReaderAsync(
string? profileName,
int? operationTypeFilter,
CancellationToken ct);
}

/// <summary>
/// An async-job reader bound to an already-open environment connection. Dispose
/// to close the connection.
/// </summary>
public interface IAsyncJobLogReader : IDisposable
{
Task<IReadOnlyList<AsyncJobRecord>> ReadAsync(EnvironmentLogFilter filter, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using TALXIS.CLI.Core.Contracts.Dataverse;

namespace TALXIS.CLI.Features.Environment.Diagnostics;

/// <summary>
/// Parses the shared environment-log CLI flags into an <see cref="EnvironmentLogFilter"/>.
/// Centralizes <c>--since</c> and <c>--correlation-id</c> validation so every leaf command
/// behaves identically. Returns <c>false</c> with a user-facing <paramref name="error"/>
/// message on malformed input.
/// </summary>
internal static class EnvLogFilterBuilder
{
private const int DefaultTop = 50;
private const int DefaultTopWithSince = 200;

public static bool TryBuild(
string? since,
string? entity,
string? plugin,
bool errorsOnly,
string? correlationId,
int? top,
out EnvironmentLogFilter filter,
out string? error)
{
filter = null!;
error = null;

DateTime? sinceUtc = null;
if (!string.IsNullOrWhiteSpace(since))
{
if (!DeploymentRelativeTimeParser.TryParse(since, out var window))
{
error = $"Invalid --since value '{since}'. Use NNNm, NNNh, NNNd, or NNNw.";
return false;
}
sinceUtc = DateTime.UtcNow - window;
}

Guid? correlation = null;
if (!string.IsNullOrWhiteSpace(correlationId))
{
if (!Guid.TryParse(correlationId.Trim(), out var parsed))
{
error = $"Invalid --correlation-id value '{correlationId}'. Expected a GUID.";
return false;
}
correlation = parsed;
}

int effectiveTop = top is > 0
? top.Value
: (sinceUtc is null ? DefaultTop : DefaultTopWithSince);

filter = new EnvironmentLogFilter(
SinceUtc: sinceUtc,
Entity: string.IsNullOrWhiteSpace(entity) ? null : entity.Trim(),
Plugin: string.IsNullOrWhiteSpace(plugin) ? null : plugin.Trim(),
ErrorsOnly: errorsOnly,
CorrelationId: correlation,
Top: effectiveTop);
return true;
}
}
Loading