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 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
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
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);
}
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;