Command tracing

Main purpose: easier support and debugging.

Command

All changes to the data are modelled as commands.

We’ve added tracing of (almost) all the commands that are executed by Finsta.

Command interface

Command.java

Command implementation example

CreateFinancialStatementCommand.java
public class CreateFinancialStatementCommand implements Command, DryRunEnabled, ForkEnabled {


    @Nullable private ForkSource forkFrom;

    @NonNull @ValidOrgNo private String orgNo;
    @NonNull @NotNull private Integer year;
    @Nullable SpecVersion specVersion;

    @Nullable private String name;
    @Nullable private String description;

    @Nullable @Valid private List<BalanceAccount> accounts;
    @Nullable private ExternalSystemReference accountingSystemReference;

    @FunctionalInterface
    public interface BalanceAccountsProvider {

        @NonNull
        List<BalanceAccount> parseFromCsvs(@Nullable String currentCsv, @Nullable String previousCsv);

    }

    @LegacyJsonSupport
    private void setCurrentCsv(@Nullable String currentCsv) {
        setTbParams(CreateTrialBalanceParams.merge(getTbParams(), "currentCsv", currentCsv));
    }

    @LegacyJsonSupport
    private void setPreviousCsv(@Nullable String previousCsv) {
        setTbParams(CreateTrialBalanceParams.merge(getTbParams(), "previousCsv", previousCsv));
    }

    @LegacyJsonSupport
    private void setCurrentCsvFileUuid(@Nullable String s) {
        if (s == null || s.isBlank()) {
            return;
        }
        setTbParams(CreateTrialBalanceParams.merge(getTbParams(), "currentCsvFileUuid", s));
    }

    @LegacyJsonSupport
    private void setPreviousCsvFileUuid(@Nullable String s) {
        if (s == null || s.isBlank()) {
            return;
        }
        setTbParams(CreateTrialBalanceParams.merge(getTbParams(), "previousCsvFileUuid", s));
    }
}

Framework

Annotation

The annotation can be placed on class level or method level.

CommandTracing.java

Interceptor

AOP around advice, targets the method invocation.

The interceptor finds the (first) command parameter and registers/initiates the tracing linked to the current request.

It also handles tracing directly when there’s no current request (PubSub), and publishes an application event.

CommandTracingInterceptor.java

Filter

Captures the result of an HTTP request, publishes application event.

CommandTracingFilter.java

Persister

The persister listens to the application events and persists the commands to the command table in the global schema.

CommandTracingPersister.java

Add tracing

Add to class

FinancialStatementController.java
@CommandTracing
@ExecuteOn(TaskExecutors.IO)
@Secured(GrantedAuthority.GA_FINSTA_WRITE)
@Controller("/financial-statements")
@Tag(name = TagName.FINANCIAL_STATEMENT)
public class FinancialStatementController {

    /// ...

}

Add to method (allows override)

Override on method level
@Post("/:synchronize")
@CommandTracing(importance = CommandImportance.Low)
@Secured({GrantedAuthority.GA_FINSTA_WRITE, GrantedAuthority.GA_FINSTA_SYNCHRONIZE})
public SyncResponse synchronizeFinancialStatement(@Valid @Body SynchronizeFinancialStatementCommand cmd) {
    return syncService.synchronize(cmd);
}

Disable on method

Override on method level
@Post("/:create-draft")
@CommandTracing(enabled = false)
@Secured(GrantedAuthority.GA_FINSTA_WRITE)
public CreateFinancialStatementCommandDraft createFinancialStatementDraft(@Valid @Body CreateFinancialStatementCommand cmd) {
    return fsService.createDraft(cmd);
}

Frontend

Demo…​

Sql examples

Here’s some examples of stuff we’ve used the tracing for.

General queries

find by tenantId
select c.*
from global.command c
where c.tenant_id = '<tenantId>'
order by created_at desc
limit 500;
find most recent failures
select c.*
from global.command c
where c.http_status_code >= 500
order by created_at desc
limit 50;
find most recent tenants
select
    timestamp_to_local_date(c.created_at) created_at_date,
    c.cmd_body ->> 'orgNo' as org_no,
    c.user_id,
    c.tenant_id
from global.command c
where c.cmd_class = 'tritt.finsta.api.tenant.CreateTenantCommand'
order by c.created_at desc
limit 100

Finsta specific queries

find fs created (by createdAt desc)
select
    timestamp_to_local_date(c.created_at) created_at_date,
    c.cmd_body ->> 'orgNo' as org_no,
    c.user_id,
    c.tenant_id,
    c.http_status
from global.command c
where c.cmd_class = 'tritt.finsta.api.finsta.CreateFinancialStatementCommand'
  and c.cmd_body -> 'options' ? 'DryRun' = false
order by c.created_at desc
limit 10;
find fs created (by lastCreatedAt desc)
with financial_statement_created as (
  select
      substring((c.created_at at time zone 'Europe/Oslo')::varchar, 1, 16) created_at_str,
      c.cmd_body ->> 'orgNo' as org_no,
      c.user_id,
      c.tenant_id,
      row_number() over (partition by c.tenant_id order by c.created_at) as row_no
  from global.command c
  where c.cmd_class = 'tritt.finsta.api.finsta.CreateFinancialStatementCommand'
    and c.http_status = 'OK'
    and (not c.cmd_body -> 'options' ? 'DryRun')
    and c.cmd_body @> '{"year": 2023}'
)
select
    org_no,
    min(created_at_str) first_created,
    max(created_at_str) last_created,
    max(row_no) num_created,
    tenant_id
from financial_statement_created
group by org_no, tenant_id
order by 3 desc
limit 30
find new tenants created per day
select
    substring(date_trunc('day', c.created_at, 'Europe/Oslo')::varchar, 1, 10) as dato,
    count(*) as new_tenants
from global.command c
where c.created_at >= '2024-04-15'
  and c.cmd_class like '%.CreateTenantCommand'
group by 1
order by 1 desc;
find commands for real estate
with command_real_estate as (
  select
      c.created_at,
      c.tenant_id,
      c.cmd_body ->> 'assetUuid' as asset_uuid,
      c.cmd_body,
      c.cmd_body
          -> 'realEstate'
          -> 'realEstateAsWealthObject'
          -> 'specificationOfCommercialProperty' as com_prop
  from global.command c
  where c.created_at >= '2024-05-01'
--     and c.tenant_id = '_99483060064628352ec94' -- delo eiendom as
    and c.tenant_id = '_888743162e25bcee92602'
    and c.cmd_class = 'tritt.finsta.api.assreg.UpdateFixedAssetCommand'
  order by c.created_at desc
)
select
    cre.tenant_id,
    cre.asset_uuid,
    count(*)
from command_real_estate cre
where cre.com_prop ->> 'isNotOnStockExchangeAndNotLocatedInALargeMunicipality' = 'Yes'
group by 1, 2
order by 1, 3 desc;
find num fs created by origin
select
    case when c.cmd_body ? 'currentCsv' then 'finsta' else 'conta diy' end as origin_system,
    c.cmd_body ->> 'year' as fs_year,
    count(*) as cnt
from global.command c
where c.cmd_class = 'tritt.finsta.api.finsta.CreateFinancialStatementCommand'
  and not (c.cmd_body ? 'options' and c.cmd_body -> 'options' ? 'DryRun')
  and c.http_status_code = 200
group by 1, 2
order by 2, 1;