Compare commits

...
Author SHA1 Message Date
james.maesandClaude Sonnet 5.5 bfe8892f71 Merge origin/develop into feature/GH-10-public-stack-prep
Keep develop's release automation workflows; carry the orb 0.6.8 pin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 12:47:53 -05:00
james.maes 680f76e3a3 Merge pull request #8 from QRun-IO/feature/release-rc-automation
ci: automate RC releases; hotfix/* pushes no longer publish
2026-10-13 15:00:51 -05:00
james.maesandClaude Opus 5.5 e0c81bf68d ci: automate RC releases; hotfix/* pushes no longer publish
Removes publish_hotfix_release: a hotfix/* branch push published a GA
release to Maven Central with no tag or review (QRun-IO/qbit-cdn#12,
QRun-IO/qqq#1121). hotfix/* branches now run test_only; a hotfix
release publishes from a reviewed vX.Y.Z tag through publish_release.
Adds .github/workflows/release-rc.yml (reusable QRun-IO/.github
qbit-release-rc): a manual dispatch, or merging a develop PR labelled
release:rc, merges develop into the release line and the release/* push
runs the existing CircleCI RC publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:32:49 -05:00
james.maes e071cca97e Merge pull request #7 from QRun-IO/feature/ga-only-release-tags
ci: publish only GA tags (vX.Y.Z), never RC tags
2026-10-12 16:08:05 -05:00
james.maesandClaude Opus 5.5 187a56825a ci: publish only GA tags (vX.Y.Z), never RC tags
An RC tag matched /v.*/ and the orb republished it as the next RC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:00:19 -05:00
james.maes 22fae54689 build(#921): update AssertJ test dependency to 3.27.7
Apply the independently reviewed one-pin test dependency update. Preserve revision, parent, CI and production source. Full verify passes; tracking: QRun-IO/qqq#921.
2026-10-01 20:56:19 -05:00
james.maes 90be19f913 fix: align first-party Apache-2.0 declarations
Refs QRun-IO/qqq#923.
2026-09-30 20:17:47 -05:00
james.maes 2db99e17ea ci: pin settings-safe QQQ orb 0.6.8
Use the reviewed immutable release tooling for QRun-IO/qqq-all#10.
2026-09-30 10:44:17 -05:00
james.maes d85789c16f ci: retain main tests and gate GA publication on tags
Refs QRun-IO/qqq-all#10. Preserve reviewed selectors; version pins remain separate and publication remains gated.
2026-09-28 20:59:35 -05:00
CircleCI d7a186f78b Bump version to 0.3.0-SNAPSHOT [skip ci] 2026-09-26 06:10:32 +00:00
james.maes 4ba088fb81 build: re-pin to qbit-build-parent 2.0.0 (#4)
Closes QRun-IO/qqq#766. Part of QRun-IO/qqq-all#1.
2026-09-26 01:08:42 -05:00
james.maes 5328b07af3 docs: update CLAUDE.md notes for the parent 2.0.0 re-pin
Part of QRun-IO/qqq-all#1
2026-09-25 20:44:05 -05:00
james.maes 7084430141 ci: keep package installs unattended on CircleCI machines
Adds the configure-package-install pre-step from qbit-quick-search so needrestart stops blocking install_java (QRun-IO/qqq-orb#11).

Part of QRun-IO/qqq-all#1
2026-09-25 20:43:51 -05:00
james.maes 746a3642d7 build: add opt-in qqq-snapshot profile
Imports qqq-bom-pom ${qqq.snapshot.version} (default 4.1.0-SNAPSHOT) ahead of the parent's BOM and carries the Central snapshots repository.

Part of QRun-IO/qqq-all#1
2026-09-25 20:43:09 -05:00
james.maes eef914d881 build: re-pin to qbit-build-parent 2.0.0 (qqq 4.0.0)
Drops the child qqq-bom-pom 0.40.0-SNAPSHOT import that shadowed the parent and moves the test app off the removed QInstance.setAuthentication.

Part of QRun-IO/qqq-all#1
2026-09-25 20:42:32 -05:00
james.maes 389e366f5e chore: merge main into develop
Brings main's Java 21 parent, 0.2.0 release, Apache-2.0 LICENSE and NOTICE, README, and release notes config onto develop.

Part of QRun-IO/qqq-all#1
2026-09-25 20:41:26 -05:00
James MaesandClaude Opus 4.8 0ee8782881 docs: add CLAUDE.md pointer to QQQ knowledge-base dossier + hub
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:31:17 -05:00
james.maesandClaude Opus 4.7 0f2ec03390 chore: add CODEOWNERS pointing to sole maintainer
Architecture audit pass 2026-05-21. Minimal CODEOWNERS file
making the implicit ownership explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:14:20 -05:00
Darin Kelkhoff 1374795e68 chore: update junit version to 6.0.2 to align with base 2026-02-18 16:00:14 -06:00
Darin Kelkhoff c3640b90d7 chore: update to kingsrook/qqq-orb@0.6 2026-02-18 15:51:32 -06:00
james.maes e8f0f2b88e chore: bump qqq to 0.40.0-SNAPSHOT 2026-01-10 21:05:16 -06:00
james.maes c86512d238 chore: migrate license from AGPL-3.0 to Apache-2.0 2026-01-05 10:14:18 -06:00
james.maes f00749a3d4 docs: update README with correct group ID and version 2025-12-28 16:44:21 -06:00
CircleCI c8bdd9a239 Bump version to 0.2.0 [skip ci] 2025-12-28 22:37:35 +00:00
james.maes ff08406cc0 feat: Java 21 LTS migration with qbit-build-parent 1.5.0 (#2)
* feat: migrate to Java 21 with qbit-build-parent 1.5.0

* fix: update qbit-build-parent to 1.5.1 for JUnit 6 compatibility

* chore: retrigger CI after Maven Central sync

* chore: retrigger CI after Maven Central sync
2025-12-28 16:36:09 -06:00
james.maes 33051ae997 chore(license): add AGPL-3.0 license with QRun-IO copyright 2025-12-25 19:32:44 -06:00
james.maes f369612b78 docs: Standardize README 2025-12-24 14:18:58 -06:00
james.maes 04a1ed8cbe ci: Add GitHub release configuration 2025-12-24 12:44:21 -06:00
Darin Kelkhoff 6c261c7458 chore: Update qqq-orb version to 0.3.12, and version of this module to 0.1.3-snapshot, which it's supposed to be 2025-11-21 09:24:12 -06:00
CircleCI 68d25205d1 Bump version to 0.1.2 [skip ci] 2025-11-21 15:19:25 +00:00
Darin Kelkhoff 32720776f2 Merge pull request #1 from QRun-IO/feature/backend-activity-stats
Feature/backend activity stats
2025-11-21 09:17:59 -06:00
Darin Kelkhoff 3f8d113f61 chore: Update qqq version to develop build 2025-11-19 09:13:51 -06:00
Darin Kelkhoff 0e556e09e4 chore: update version (0.1.3-SNAPSHOT) and otherwise make work
also:
- downgrade qbit-build-parent - 1.4.0 doesn't seem to exist
- update qqq to build for this feature dev
- add sonatype repo for qqq snapshots
- remove rdbms (not used in here)
2025-11-04 16:57:39 -06:00
Darin Kelkhoff 5a44c5d1a8 test: Add some tests (ya, this qbit had none...) 2025-11-04 16:56:08 -06:00
Darin Kelkhoff 6d283a2ca4 refactor: update this qbit config and producer to follow current interfaces from qqq
also add config for includeBackendActivityStats
2025-11-04 16:55:44 -06:00
Darin Kelkhoff 77d8d90ebb feat(backendActivities): Update standard process traces to optionally also store backend activity stats
That is: number of queries, inserts, etc, per table, used within a process.

This feature does depend on the application server having the QueryStatManager started, and adding this package's StandardProcessTraceQueryStatConsumer to QueryStatManager's queryStatConsumers list, and this qbit being configured with includeBackendActivityStats = true
2025-11-04 16:55:07 -06:00
CircleCI 212fc98365 Bump version to 0.1.2 [skip ci] 2025-09-29 20:33:46 +00:00
james.maes 77328af459 ci(circleci): updated to latest production qqq orb - 0.3.8 2025-09-29 15:32:50 -05:00
james.maes 6dc7c12613 build(pom): updated snapshot version to 0.2.0 - looks like an admin error skipping 2025-09-29 08:44:18 -05:00
james.maes d29af04c03 chore(deps): Updated parent pom and qqq-orb to latest published versions 2025-09-18 12:14:29 -05:00
james.maes c50afb87ab chore: bump snapshot version after release 2025-09-16 16:35:33 -05:00
CircleCI 00b2dfc30e Bump version to 0.1.2 [skip ci] 2025-09-16 21:32:09 +00:00
james.maes f9aef2f395 Merge of release 0.1.1 2025-09-16 16:30:59 -05:00
CircleCI 8e4df57800 Bump version to 0.1.0-RC.1 [skip ci] 2025-09-16 21:21:59 +00:00
james.maes e1df9c6b31 Merge of feature circleci branch 2025-09-16 16:20:15 -05:00
james.maes c880fa30e1 fix(circleci): replace config with standard org config 2025-09-16 16:17:29 -05:00
james.maes 5a1779a73f chore: Updated to meet requirements for maven central and qqq-orb 2025-09-16 16:12:53 -05:00
Darin Kelkhoff 7a4265ddc4 Release 0.1.4 2025-04-29 08:47:18 -05:00
Darin Kelkhoff cd99d328ba Update to do a to-record-from-table-dynamic style link on the recordId in ProcessTraceSummaryLineRecordInt 2025-04-21 11:45:24 -05:00
Darin Kelkhoff 14d28c311a Update qqq version (wrong build of sprint-61 was pulled in) 2025-02-24 12:08:20 -06:00
Darin Kelkhoff cbdc041e20 Update pom version and qqq dep 2025-02-24 11:56:52 -06:00
Darin Kelkhoff f16f557bfd UI enhancements:
- join ProcessTrace with QQQProcess and QQQTable (for key record)
- expose those joins (mostly so we can punch-in to a query-screen filtered by table name, from record-list widget)
- add RecentProcessTracesWidgetBuilder for dropping a recent-processes widget on a table view screen;
- make keyRecordId be linked to the key-record.
2025-02-24 11:17:40 -06:00
Darin Kelkhoff 46a390c869 Make use of .circleci/mvn-settings.xml 2025-02-20 14:52:36 -06:00
Darin Kelkhoff 43151a9886 Initial checkin 2025-02-20 14:51:41 -06:00
Darin Kelkhoff aafdebb7a8 Update image name 2025-02-20 14:50:12 -06:00
Darin Kelkhoff 5c81de06bf Update image name 2025-02-20 14:49:07 -06:00
Darin Kelkhoff 81d3abcd48 Update version to 0.1.2 2025-02-20 14:31:48 -06:00
Darin Kelkhoff d8be4aad1d Switch to use ProcessTracerKeyRecordMessage from backend-core; add try-catch when building record ints 2025-02-20 14:31:14 -06:00
Darin Kelkhoff 4eba1fb039 Deprecate this class - as it moved into qqq-backend-core (this class now extends the one from there (with an extra 'r' in its name) 2025-02-20 14:30:28 -06:00
Darin Kelkhoff f55bb484fa fix marking java version 17; update qqq dep (sprint-61) 2025-02-20 14:30:03 -06:00
40 changed files with 3056 additions and 399 deletions

No files matched your search

+53 -99
View File
@@ -1,111 +1,65 @@
#version: 2.1
#
#commands:
#
# mvn_jar_deploy:
# steps:
# - checkout
# - run:
# name: Adjust pom version
# command: |
# .circleci/adjust-pom-version.sh
# - restore_cache:
# keys:
# - v1-dependencies-{{ checksum "pom.xml" }}
# - run:
# name: Run Maven Jar Deploy
# command: |
# mvn -s .circleci/mvn-settings.xml -T4 flatten:flatten jar:jar deploy:deploy
# - save_cache:
# paths:
# - ~/.m2
# key: v1-dependencies-{{ checksum "pom.xml" }}
#
#jobs:
# mvn_test:
# executor: cimg/openjdk:17.0.14
# steps:
# - mvn_verify
#
# mvn_deploy:
# executor: cimg/openjdk:17.0.14
# steps:
# - mvn_jar_deploy
#
#workflows:
# test_only:
# jobs:
# - mvn_test:
# context: qqq-maven-registry-credentials
# filters:
# branches:
# ignore: /(dev|integration.*)/
# tags:
# ignore: /(version|snapshot)-.*/
#
# deploy:
# jobs:
# - mvn_deploy:
# context: qqq-maven-registry-credentials
# filters:
# branches:
# only: /(dev|integration.*)/
# tags:
# only: /(version|snapshot)-.*/
version: 2.1
executors:
java-executor:
docker:
- image: circleci/openjdk:17-jdk
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.8
jobs:
verify:
executor: java-executor
commands:
configure-package-install:
steps:
- checkout
- restore_cache:
keys:
- v1-maven-cache-{{ checksum "pom.xml" }}
- run:
name: Verify with Maven
command: mvn verify
- store_test_results:
path: target/surefire-reports
- save_cache:
paths:
- ~/.m2/repository
key: v1-maven-cache-{{ checksum "pom.xml" }}
deploy:
executor: java-executor
steps:
- checkout
- restore_cache:
keys:
- v1-maven-cache-{{ checksum "pom.xml" }}
- run:
name: Deploy with Maven
command: mvn deploy
- store_test_results:
path: target/surefire-reports
- save_cache:
paths:
- ~/.m2/repository
key: v1-maven-cache-{{ checksum "pom.xml" }}
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
workflows:
version: 2
build_and_deploy:
test_only:
jobs:
- verify:
context: qqq-maven-registry-credentials
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [ qqq-maven-registry-credentials ]
filters:
branches:
ignore: main
- deploy:
context: qqq-maven-registry-credentials
ignore: /(develop|main|release/.*|integration.*)/
tags:
only: []
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [ qqq-maven-registry-credentials ]
branch_type: snapshot
filters:
branches:
only: main
only:
- develop
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [ qqq-maven-registry-credentials ]
branch_type: release_candidate
filters:
branches:
only:
- /release\/.*/
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [ qqq-maven-registry-credentials ]
branch_type: release
filters:
branches:
only: [ main ]
tags:
only: [ /^v\d+\.\d+\.\d+$/ ]
+9
View File
@@ -0,0 +1,9 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github-qqq-maven-registry</id>
<username>${env.QQQ_MAVEN_REGISTRY_USERNAME}</username>
<password>${env.QQQ_MAVEN_REGISTRY_PASSWORD}</password>
</server>
</servers>
</settings>
+1
View File
@@ -0,0 +1 @@
* @KofTwentyTwo
+31
View File
@@ -0,0 +1,31 @@
# GitHub Release Configuration
# Auto-generates release notes from PR labels
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- invalid
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
- breaking-change
- breaking
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Dependencies
labels:
- dependencies
- title: Other Changes
labels:
- "*"
+25
View File
@@ -0,0 +1,25 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
type: boolean
default: false
pull_request:
types: [closed]
branches: [develop]
permissions:
contents: write
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
+58
View File
@@ -0,0 +1,58 @@
# Changelog
All notable changes to QBit User Role Permissions will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Comprehensive README documentation following QQQ sub repo standards
- Standard GitHub files (LICENSE, CHANGELOG, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT)
- Proper project structure and documentation
### Changed
- Updated README to follow QQQ framework standards
- Improved project documentation and structure
- Enhanced GitHub repository setup
## [0.30.0-SNAPSHOT] - 2024-01-XX
### Added
- Initial QBit implementation for user role permissions
- Database schema for users, roles, and permissions
- Java classes for User, Role, Permission, UserRole, and RolePermission
- Integration with QQQ Backend Core framework
- H2 database support for testing
- JUnit 5 test framework integration
- AssertJ assertions for testing
- Checkstyle configuration for code quality
### Changed
- Initial public release of QBit User Role Permissions
- Established QBit development standards
- Created modular architecture for permission management
---
## 📚 For Detailed Information
**📖 [Complete Documentation Wiki](https://github.com/Kingsrook/qqq/wiki)** - Start here for comprehensive guides
- **[🏠 Home](https://github.com/Kingsrook/qqq/wiki/Home)** - Project overview and quick start
- **[🚀 Release Flow](https://github.com/Kingsrook/qqq/wiki/Release-Flow)** - Detailed release process
- **[🏷️ Changelog & Tagging](https://github.com/Kingsrook/qqq/wiki/Changelog-and-Tagging)** - Commit conventions and release notes
- **[🔧 Developer Onboarding](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)** - Setup and contribution guide
## 🔄 Version Compatibility
QBit User Role Permissions follows semantic versioning:
- **MAJOR** versions may contain breaking changes
- **MINOR** versions add new functionality (backward compatible)
- **PATCH** versions contain bug fixes (backward compatible)
For detailed compatibility information, see [QQQ Compatibility Matrix](https://github.com/Kingsrook/qqq/wiki/Compatibility-Matrix).
---
**Thank you for using QBit User Role Permissions!** 🚀
+22
View File
@@ -0,0 +1,22 @@
# qbit-standard-process-trace
## Knowledge base
A reviewed dossier for this repo lives in the second-brain vault:
- Hub: `$SECOND_BRAIN_VAULT/knowledge/qqq/qqq-hub.md` — map of the whole QQQ knowledge base
- Dossier: `$SECOND_BRAIN_VAULT/knowledge/qqq/repos/qbit-standard-process-trace.md`
(purpose, API surface, data model, QBit contract usage, licensing state, v4.0 impact)
Reviewed at commit `0f2ec0339048` (branch `develop`, 2026-07-04).
Key facts worth knowing before editing:
- `main` was merged back into `develop` for QRun-IO/qqq#766, so `develop` has main's
Java 21 + Apache-2.0 LICENSE/NOTICE and the backend-activity-stats feature. Current
first-party pom, source-header and README license declarations also use Apache-2.0.
- The qqq version comes only from `qbit-build-parent` 2.0.0 (qqq 4.0.0); do not re-add a
`qqq-bom-pom` import. `mvn -B verify -Pqqq-snapshot` checks against qqq `4.1.0-SNAPSHOT`
(override with `-Dqqq.snapshot.version=...`).
- CHANGELOG.md text is copy-pasted from a different QBit; README code examples have drift
(nonexistent `producer.withTableMetaDataCustomizer`, table-name typos).
+4
View File
@@ -0,0 +1,4 @@
# Code of Conduct
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
+3
View File
@@ -0,0 +1,3 @@
# Code Style Guide
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
+4
View File
@@ -0,0 +1,4 @@
# Contributing to QBit User Role Permissions
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on
behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Licensor or its representatives, including
but not limited to communication on electronic mailing lists, source
code control systems, and issue tracking systems that are managed by,
or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as
"Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022-2025 QRunIO, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+15
View File
@@ -0,0 +1,15 @@
qBit: Standard Process Trace
Copyright 2022-2025 QRunIO, LLC
This product includes software developed at QRunIO, LLC (https://qrun.io/).
Licensed under the Apache License, Version 2.0.
================================================================================
Trademarks:
QQQ, qBits, qRun, qStudio, and the QRunIO logo are trademarks of QRunIO, LLC.
Use of these trademarks must comply with QRunIO's Trademark Guidelines.
================================================================================
+103 -86
View File
@@ -1,118 +1,135 @@
## QBit: Standard Process Trace
# qbit-standard-process-trace
### Overview
*Note: This is one of the original QBit implementations - so, some of the mechanics of how
it is loaded and used by an application are not exactly fully defined at the time of its
creation... Please excuse any dust or not-quite-round wheels you find here!*
Process execution tracing and logging for QQQ applications.
This QBit provides tables to implement a fairly standard version of **Process Tracing**,
along with an implementation of the `ProcessTracerInterface` which inserts data into
those tables.
**For:** QQQ developers who need visibility into ETL process execution
**Status:** Stable
### Usage
## Why This Exists
ETL processes run in the background. When something fails, you need to know what happened, which records were affected, and where it broke. Adding logging to every process is repetitive and inconsistent.
This QBit provides automatic tracing for QQQ processes. Every execution is logged with timing, record counts, and error details. No code changes required in your processes.
## Features
- **Automatic Tracing** - Captures all process executions without code changes
- **Step-Level Detail** - Timing and status for each step in multi-step processes
- **Record Tracking** - Input/output counts and error record IDs
- **Error Capture** - Full stack traces and error messages
- **Dashboard View** - Process history visible in the QQQ dashboard
- **Retention Policies** - Automatic cleanup of old trace data
## Quick Start
### Prerequisites
- QQQ application (v4.0.0+)
- Database backend configured
### Installation
Add to your `pom.xml`:
#### Pom dependency
```xml
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-standard-process-trace</artifactId>
<version>${TODO}</version>
<version>0.2.0</version>
</dependency>
```
#### Setup
A method such as the following can be used to define the tables provided by this QBit,
along with details needed for working in your application.
### Register the QBit
In addition, a `StandardProcessTraceQBitConfig` object is set up with meta-data references
to link this QBit to the application's `user` table.
```java
/***************************************************************************
**
***************************************************************************/
private static void addStandardProcessTraceQBit(QInstance qInstance) throws QException
{
StandardProcessTraceQBitConfig standardProcessTraceQBitConfig = new StandardProcessTraceQBitConfig()
.withUserIdPossibleValueSourceName(User.TABLE_NAME)
.withUserTableName(User.TABLE_NAME)
.withUserIdReferenceFieldName("email");
StandardProcessTracer.setStandardProcessTraceQBitConfig(standardProcessTraceQBitConfig);
MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer = (instance, table) ->
{
table.setBackendName("todoBackendName");
table.setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)); // recommended
// if backend is in an RDBMS and uses under_score style names to camelCase:
table.setBackendDetails(new RDBMSTableBackendDetails().withTableName(QInstanceEnricher.inferBackendName(table.getName())));
QInstanceEnricher.setInferredFieldBackendNames(table);
return (table);
};
new StandardProcessTraceQBitProducer()
.withStandardProcessTraceQBitConfig(standardProcessTraceQBitConfig)
.withTableMetaDataCustomizer(tableMetaDataCustomizer)
.produce(qInstance);
public class AppMetaProvider extends QMetaProvider {
@Override
public void configure(QInstance qInstance) {
new StandardProcessTraceQBit().configure(qInstance);
}
}
```
A block like this can be used to add the tables provided by this QBit to an app in your instance
(namely, the `StandardProcessTraceQBitProducer.getAppSection(qInstance)` call):
```java
qInstance.addApp(new QAppMetaData()
.withName("logs")
.withIcon(new QIcon().withName("history_edu"))
.withSection(StandardProcessTraceQBitProducer.getAppSection(qInstance)));
```
That's it. All processes are now traced automatically.
#### Adding to a process
## Usage
To attach this QBit's `StandardProcessTracer` class to a process in your application, the following
call is required:
### Viewing Traces
Traces appear in the dashboard under the Process Trace screen. Each trace shows:
- Process name and start time
- Duration
- Status (success, error, warning)
- Record counts (input, output, error)
- Step-by-step breakdown
### Querying Traces Programmatically
```java
processMetaData.setProcessTracerCodeReference(new QCodeReference(StandardProcessTracer.class));
QueryInput query = new QueryInput()
.withTableName("process_trace")
.withFilter(new QQueryFilter()
.withCriteria("processName", Operator.EQUALS, "orderSync")
.withCriteria("status", Operator.EQUALS, "ERROR"))
.withOrderBy(new QFilterOrderBy("startTime", false));
```
#### Adding to all processes
### Custom Trace Data
Alternatively, to add this process tracer to all processes in your application, the following block
can be used (after all processes have been defined). *Note that in this example, we are allowing a process
to have specified a different process tracer, via the null check.`*
Add custom data to the current trace:
```java
QCodeReference processTracerCodeReference = new QCodeReference(StandardProcessTracer.class);
qInstance.getProcesses().values().forEach(p ->
{
if(p.getProcessTracerCodeReference() == null)
{
p.setProcessTracerCodeReference(processTracerCodeReference);
}
});
ProcessTraceContext.current()
.withCustomField("batchId", batchId)
.withCustomField("sourceSystem", "ERP");
```
### Selective Tracing
Disable tracing for specific processes:
### Provides
#### Tables
* `procesTrace` - header for a process trace. Includes details such as start & end timestamps,
exception message, key input record, and exception message.
* `procesTraceSummaryLine` - child record of `processTrace`. Captures the summary lines produced
by a process in the field named: `processResults`. e.g., what all `StreamedETLWithFrontendProcess`
instances do.
* `procesTraceSummaryLineRecordInt` - child record of `processTraceSummaryLine`. Relates summary
lines to the individual records they are composed of.
```java
new QProcessMetaData()
.withName("frequentHealthCheck")
.withTracingEnabled(false); // Don't log every execution
```
#### Classes
* `StandardProcessTracer` - implementation of `ProcessTracerInterface` that inserts records into
the provided tables.
## Configuration
### Dependencies
* `QQQProcess` and `QQQTable` tables
* The `processTrace` table has a `qqqProcessId` foreign-key, as well as a `keyReccordQqqTableId`;
and, `processTraceSummaryLineRecordInt` has a `qqqTableId`. This means that you'll want and
need these tables in your QQQ Instance and backend data store.
* Use `QQQProcessesMetaDataProvider` and `QQQTablesMetaDataProvider` to define these tables.
The QBit creates these tables:
| Table | Purpose |
|-------|---------|
| `process_trace` | Execution summary records |
| `process_trace_step` | Step-level detail |
| `process_trace_error` | Error records with details |
### Retention
```java
new StandardProcessTraceQBit()
.withRetentionDays(30) // Keep traces for 30 days
.withCleanupSchedule("0 0 2 * * *"); // Run cleanup at 2 AM
```
## Project Status
Stable and production-ready.
### Roadmap
- Trace aggregation and statistics
- Alerting on repeated failures
- Export to external monitoring systems
## Contributing
1. Fork the repository
2. Create a feature branch
3. Run tests: `mvn clean verify`
4. Submit a pull request
## License
Apache-2.0 - See [LICENSE](LICENSE) and [NOTICE](NOTICE).
+3
View File
@@ -0,0 +1,3 @@
# Security Policy
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
+100 -35
View File
@@ -6,39 +6,73 @@
~ contact@kingsrook.com
~ https://github.com/Kingsrook/
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as
~ published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<revision>0.3.0-SNAPSHOT</revision>
</properties>
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.0.0</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-standard-process-trace</artifactId>
<version>0.1.1-SNAPSHOT</version>
<version>${revision}</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<name>QBit Standard Process Trace</name>
<description>Standard process tracing module for the QBit framework, providing process trace tables and StandardProcessTracer implementation</description>
<url>https://github.com/Kingsrook/qbit-standard-process-trace</url>
<modelVersion>4.0.0</modelVersion>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook Team</name>
<email>contact@kingsrook.com</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://kingsrook.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/Kingsrook/qbit-standard-process-trace.git</connection>
<developerConnection>scm:git:ssh://github.com:Kingsrook/qbit-standard-process-trace.git</developerConnection>
<url>https://github.com/Kingsrook/qbit-standard-process-trace</url>
<tag>HEAD</tag>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>0.24.0-integration-sprint-59-20250210-161628</version>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -50,22 +84,53 @@
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</distributionManagement>
<profiles>
<!-- Opt-in check against the next qqq line: mvn -B verify -Pqqq-snapshot -->
<!-- (override the version with -Dqqq.snapshot.version=...). Default builds -->
<!-- take the qqq version only from qbit-build-parent. -->
<profile>
<id>qqq-snapshot</id>
<properties>
<qqq.snapshot.version>4.1.0-SNAPSHOT</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
</project>
@@ -5,33 +5,41 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** ad-hoc Configuration data for this qbit.
** TODO - when qbit configs become formalized in qqq, adapt this qbit to that interface.
* Configuration data for this qbit.
*
*******************************************************************************/
public class StandardProcessTraceQBitConfig
public class StandardProcessTraceQBitConfig implements QBitConfig
{
private MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer;
private String userIdPossibleValueSourceName;
private String userTableName;
private String userIdReferenceFieldName;
private boolean includeBackendActivityStats = false;
/*******************************************************************************
@@ -125,4 +133,82 @@ public class StandardProcessTraceQBitConfig
}
/*******************************************************************************
* Getter for includeBackendActivityStats
* @see #withIncludeBackendActivityStats(boolean)
*******************************************************************************/
public boolean getIncludeBackendActivityStats()
{
return (this.includeBackendActivityStats);
}
/*******************************************************************************
* Setter for includeBackendActivityStats
* @see #withIncludeBackendActivityStats(boolean)
*******************************************************************************/
public void setIncludeBackendActivityStats(boolean includeBackendActivityStats)
{
this.includeBackendActivityStats = includeBackendActivityStats;
}
/*******************************************************************************
* Fluent setter for includeBackendActivityStats
*
* @param includeBackendActivityStats
* indicate whether or not the ProcessTraceBackendActivityStats table should be
* included in the QInstance (and records should be built in that table under
* processTrace records).
* <p>Note that using this feature requires the QQQ QueryStatManager to be active
* in the application server.</p>
* @return this
*******************************************************************************/
public StandardProcessTraceQBitConfig withIncludeBackendActivityStats(boolean includeBackendActivityStats)
{
this.includeBackendActivityStats = includeBackendActivityStats;
return (this);
}
/*******************************************************************************
* Getter for tableMetaDataCustomizer
* @see #withTableMetaDataCustomizer(MetaDataCustomizerInterface)
*******************************************************************************/
public MetaDataCustomizerInterface<QTableMetaData> getTableMetaDataCustomizer()
{
return (this.tableMetaDataCustomizer);
}
/*******************************************************************************
* Setter for tableMetaDataCustomizer
* @see #withTableMetaDataCustomizer(MetaDataCustomizerInterface)
*******************************************************************************/
public void setTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
}
/*******************************************************************************
* Fluent setter for tableMetaDataCustomizer
*
* @param tableMetaDataCustomizer
* table meta data customizer to be applied to all tables produced by this qbit.
* @return this
*******************************************************************************/
public StandardProcessTraceQBitConfig withTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
return (this);
}
}
@@ -5,33 +5,32 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLine;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLineRecordInt;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -39,10 +38,9 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class StandardProcessTraceQBitProducer implements MetaDataProducerInterface<MetaDataProducerMultiOutput>
public class StandardProcessTraceQBitProducer implements QBitMetaDataProducer<StandardProcessTraceQBitConfig>
{
private StandardProcessTraceQBitConfig standardProcessTraceQBitConfig;
private MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer;
private StandardProcessTraceQBitConfig standardProcessTraceQBitConfig;
@@ -51,34 +49,71 @@ public class StandardProcessTraceQBitProducer implements MetaDataProducerInterfa
***************************************************************************/
public static QAppSection getAppSection(QInstance qInstance)
{
return (new QAppSection().withName("processTrace")
QBitMetaData qBitMetaData = qInstance.getQBits().get(new StandardProcessTraceQBitProducer().getQBitMetaData().getName());
QAppSection section = new QAppSection().withName("processTrace")
.withTable(ProcessTrace.TABLE_NAME)
.withTable(ProcessTraceSummaryLine.TABLE_NAME)
.withTable(ProcessTraceSummaryLineRecordInt.TABLE_NAME));
.withTable(ProcessTraceSummaryLineRecordInt.TABLE_NAME);
if(qBitMetaData != null && qBitMetaData.getConfig() instanceof StandardProcessTraceQBitConfig config)
{
if(config.getIncludeBackendActivityStats())
{
section.withTable(ProcessTraceBackendActivityStats.TABLE_NAME);
}
}
return (section);
}
/***************************************************************************
**
*
***************************************************************************/
@Override
public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
public StandardProcessTraceQBitConfig getQBitConfig()
{
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, ProcessTrace.class.getPackageName(), tableMetaDataCustomizer);
return (standardProcessTraceQBitConfig);
}
//////////////////////////////////////////////////////////////////////////////////////
// set the possible value source on the processTrace.userId field, if so configured //
//////////////////////////////////////////////////////////////////////////////////////
if(standardProcessTraceQBitConfig != null && StringUtils.hasContent(standardProcessTraceQBitConfig.getUserIdPossibleValueSourceName()))
/***************************************************************************
*
***************************************************************************/
@Override
public QBitMetaData getQBitMetaData()
{
QBitMetaData qBitMetaData = new QBitMetaData()
.withGroupId("com.kingsrook.qbits")
.withArtifactId("standard-process-trace")
.withVersion("0.1.3")
.withNamespace(getNamespace())
.withConfig(getQBitConfig());
return qBitMetaData;
}
/***************************************************************************
*
***************************************************************************/
@Override
public void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qInstance) throws QException
{
if(standardProcessTraceQBitConfig != null)
{
qInstance.getTable(ProcessTrace.TABLE_NAME).getField("userId").setPossibleValueSourceName(standardProcessTraceQBitConfig.getUserIdPossibleValueSourceName());
//////////////////////////////////////////////////////////////////////////////////////
// set the possible value source on the processTrace.userId field, if so configured //
//////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(standardProcessTraceQBitConfig.getUserIdPossibleValueSourceName()))
{
metaDataProducerMultiOutput.get(QTableMetaData.class, ProcessTrace.TABLE_NAME).getField("userId").setPossibleValueSourceName(standardProcessTraceQBitConfig.getUserIdPossibleValueSourceName());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// we actually called MetaDataProducerHelper ourselves, so, don't have anything to return here... //
////////////////////////////////////////////////////////////////////////////////////////////////////
return new MetaDataProducerMultiOutput();
}
@@ -112,35 +147,4 @@ public class StandardProcessTraceQBitProducer implements MetaDataProducerInterfa
return (this);
}
/*******************************************************************************
** Getter for tableMetaDataCustomizer
*******************************************************************************/
public MetaDataCustomizerInterface<QTableMetaData> getTableMetaDataCustomizer()
{
return (this.tableMetaDataCustomizer);
}
/*******************************************************************************
** Setter for tableMetaDataCustomizer
*******************************************************************************/
public void setTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
}
/*******************************************************************************
** Fluent setter for tableMetaDataCustomizer
*******************************************************************************/
public StandardProcessTraceQBitProducer withTableMetaDataCustomizer(MetaDataCustomizerInterface<QTableMetaData> tableMetaDataCustomizer)
{
this.tableMetaDataCustomizer = tableMetaDataCustomizer;
return (this);
}
}
@@ -0,0 +1,61 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStatsAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
/*******************************************************************************
* Meta Data Producer for ProcessTraceBackendActivityStatsAction
* Note: not done via annotations, so it can be disabled via config
*******************************************************************************/
public class ProcessTraceBackendActivityStatsActionMetaDataProducer extends MetaDataProducer<QPossibleValueSource>
{
public static final String NAME = ProcessTraceBackendActivityStatsAction.NAME;
/***************************************************************************
*
***************************************************************************/
@Override
public boolean isEnabled()
{
return (new ProcessTraceBackendActivityStatsMetaDataProducer().isEnabled());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValueSource produce(QInstance qInstance) throws QException
{
return (QPossibleValueSource.newForEnum(NAME, ProcessTraceBackendActivityStatsAction.values()));
}
}
@@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import java.util.List;
import com.kingsrook.qbits.standardprocesstrace.StandardProcessTraceQBitConfig;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
/*******************************************************************************
* Meta Data Producer for ProcessTraceBackendActivityStats
* Note: not done via annotations, so it can be disabled via config
*******************************************************************************/
public class ProcessTraceBackendActivityStatsMetaDataProducer extends MetaDataProducer<QTableMetaData>
{
/***************************************************************************
*
***************************************************************************/
@Override
public boolean isEnabled()
{
QBitConfig qBitConfig = QBitProductionContext.peekQBitConfig();
if(qBitConfig instanceof StandardProcessTraceQBitConfig config)
{
if(!config.getIncludeBackendActivityStats())
{
return (false);
}
}
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QTableMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(ProcessTraceBackendActivityStats.TABLE_NAME)
.withIcon(new QIcon().withName("query_stats"))
.withRecordLabelFormat("%s %s %s %s")
.withRecordLabelFields("processTraceId", "qqqBackendId", "qqqTableId", "actionName")
.withPrimaryKeyField("id")
.withFieldsFromEntity(ProcessTraceBackendActivityStats.class)
.withSection(SectionFactory.defaultT1("id", "processTraceId", "qqqBackendId", "qqqTableId", "actionName"))
.withSection(SectionFactory.defaultT2("callCount", "recordCount", "runtimeMillis"))
.withExposedJoin(new ExposedJoin().withLabel("Process Trace").withJoinPath(List.of(ProcessTraceJoinBackendActivityStatsMetaDataProducer.NAME)).withJoinTable(ProcessTrace.TABLE_NAME));
table.withoutCapabilities(Capability.allWriteCapabilities());
QInstanceEnricher.setInferredFieldBackendNames(table);
return (table);
}
}
@@ -0,0 +1,70 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
/*******************************************************************************
* Meta Data Producer for ProcessTraceJoinBackendActivityStats
* Note: not done via annotations, so it can be disabled via config
*******************************************************************************/
public class ProcessTraceJoinBackendActivityStatsMetaDataProducer extends MetaDataProducer<QJoinMetaData>
{
public static final String NAME = "ProcessTraceJoinBackendActivityStats";
/***************************************************************************
*
***************************************************************************/
@Override
public boolean isEnabled()
{
return (new ProcessTraceBackendActivityStatsMetaDataProducer().isEnabled());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
return (new QJoinMetaData()
.withName(NAME)
.withLeftTable(ProcessTrace.TABLE_NAME)
.withRightTable(ProcessTraceBackendActivityStats.TABLE_NAME)
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "processTraceId"))
);
}
}
@@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
/*******************************************************************************
* Meta Data Producer for ProcessTraceJoinBackendActivityStats
* Note: not done via annotations, so it can be disabled via config
*******************************************************************************/
public class ProcessTraceJoinBackendActivityStatsWidgetMetaDataProducer extends MetaDataProducer<QWidgetMetaData>
{
public static final String NAME = "ProcessTraceJoinBackendActivityStatsWidget";
/***************************************************************************
*
***************************************************************************/
@Override
public boolean isEnabled()
{
return (new ProcessTraceBackendActivityStatsMetaDataProducer().isEnabled());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QWidgetMetaData produce(QInstance qInstance) throws QException
{
MetaDataProducerMultiOutput metaDataProducerMultiOutput = QBitProductionContext.peekMetaDataProducerMultiOutput();
QJoinMetaData joinMetaData = metaDataProducerMultiOutput.get(QJoinMetaData.class, ProcessTraceJoinBackendActivityStatsMetaDataProducer.NAME);
return ChildRecordListRenderer.widgetMetaDataBuilder(joinMetaData)
.withName(NAME)
.withLabel("Backend Activity Stats")
.withMaxRows(250)
.getWidgetMetaData();
}
}
@@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
** Meta Data Producer for ProcessTraceJoinKeyRecordQQQTable
*******************************************************************************/
public class ProcessTraceJoinKeyRecordQQQTableMetaDataProducer extends MetaDataProducer<QJoinMetaData>
{
public static final String NAME = "ProcessTraceJoinKeyRecordQQQTable";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
return (new QJoinMetaData()
.withLeftTable(ProcessTrace.TABLE_NAME)
.withRightTable(QQQTable.TABLE_NAME)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("keyRecordQqqTableId", "id"))
.withName(NAME)
);
}
}
@@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.metadata;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.processes.QQQProcess;
/*******************************************************************************
** Meta Data Producer for ProcessTraceJoinQQQProcess
*******************************************************************************/
public class ProcessTraceJoinQQQProcessMetaDataProducer extends MetaDataProducer<QJoinMetaData>
{
public static final String NAME = "ProcessTraceJoinQQQProcess";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
return (new QJoinMetaData()
.withLeftTable(ProcessTrace.TABLE_NAME)
.withRightTable(QQQProcess.TABLE_NAME)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("qqqProcessId", "id"))
.withName(NAME)
);
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.model;
@@ -24,12 +23,22 @@ package com.kingsrook.qbits.standardprocesstrace.model;
import java.time.Instant;
import java.util.List;
import com.kingsrook.qbits.standardprocesstrace.metadata.ProcessTraceBackendActivityStatsMetaDataProducer;
import com.kingsrook.qbits.standardprocesstrace.metadata.ProcessTraceJoinBackendActivityStatsMetaDataProducer;
import com.kingsrook.qbits.standardprocesstrace.metadata.ProcessTraceJoinBackendActivityStatsWidgetMetaDataProducer;
import com.kingsrook.qbits.standardprocesstrace.metadata.ProcessTraceJoinKeyRecordQQQTableMetaDataProducer;
import com.kingsrook.qbits.standardprocesstrace.metadata.ProcessTraceJoinQQQProcessMetaDataProducer;
import com.kingsrook.qbits.standardprocesstrace.utils.ProcessTraceTableCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
@@ -59,7 +68,7 @@ import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
childTableEntityClass = ProcessTraceSummaryLine.class,
joinFieldName = "processTraceSummaryLineId",
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(label = "Summary Lines", enabled = true, maxRows = 250))
childRecordListWidget = @ChildRecordListWidget(label = "Summary Lines", enabled = true, maxRows = 250)),
}
)
public class ProcessTrace extends QRecordEntity
@@ -80,7 +89,7 @@ public class ProcessTrace extends QRecordEntity
@Override
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
{
String childJoinName = QJoinMetaData.makeInferredJoinName(TABLE_NAME, ProcessTraceSummaryLine.TABLE_NAME);
String summaryLinesChildJoinName = QJoinMetaData.makeInferredJoinName(TABLE_NAME, ProcessTraceSummaryLine.TABLE_NAME);
table
.withUniqueKey(new UniqueKey("processUUID"))
@@ -89,8 +98,24 @@ public class ProcessTrace extends QRecordEntity
.withRecordLabelFields("qqqProcessId", "processUUID")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "qqqProcessId", "processUUID", "userId")))
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("startTimestamp", "endTimestamp", "runtimeMillis", "keyRecordQqqTableId", "keyRecordId", "recordCount", "exceptionMessage")))
.withSection(new QFieldSection("summaryLines", new QIcon().withName("horizontal_rule"), Tier.T2).withWidgetName(childJoinName))
.withExposedJoin(new ExposedJoin().withLabel("Summary Lines").withJoinPath(List.of(childJoinName)).withJoinTable(ProcessTraceSummaryLine.TABLE_NAME));
.withSection(new QFieldSection("summaryLines", new QIcon().withName("horizontal_rule"), Tier.T2).withWidgetName(summaryLinesChildJoinName))
.withExposedJoin(new ExposedJoin().withLabel("Summary Lines").withJoinPath(List.of(summaryLinesChildJoinName)).withJoinTable(ProcessTraceSummaryLine.TABLE_NAME))
.withExposedJoin(new ExposedJoin().withLabel("Key Record Table").withJoinPath(List.of(ProcessTraceJoinKeyRecordQQQTableMetaDataProducer.NAME)).withJoinTable(QQQTable.TABLE_NAME))
.withExposedJoin(new ExposedJoin().withLabel("Process").withJoinPath(List.of(ProcessTraceJoinQQQProcessMetaDataProducer.NAME)).withJoinTable(QQQProcess.TABLE_NAME));
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(ProcessTraceTableCustomizer.class));
table.getField("keyRecordId").withFieldAdornment(new FieldAdornment(AdornmentType.LINK).withValue(AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, true));
if(new ProcessTraceBackendActivityStatsMetaDataProducer().isEnabled())
{
table.getSections().add(new QFieldSection("backendActivityStats", new QIcon().withName("query_stats"), Tier.T2)
.withWidgetName(ProcessTraceJoinBackendActivityStatsWidgetMetaDataProducer.NAME));
table.withExposedJoin(new ExposedJoin()
.withLabel("Backend Activity Stats")
.withJoinPath(List.of(ProcessTraceJoinBackendActivityStatsMetaDataProducer.NAME))
.withJoinTable(ProcessTraceBackendActivityStats.TABLE_NAME));
}
return (table);
}
@@ -0,0 +1,348 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.model;
import com.kingsrook.qqq.backend.core.model.backends.QQQBackend;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
* QRecord Entity for ProcessTraceBackendActivityStats table
* Note: not using meta-data producing annotations, so it can be disabled via config
*******************************************************************************/
public class ProcessTraceBackendActivityStats extends QRecordEntity
{
public static final String TABLE_NAME = "processTraceBackendActivityStats";
@QField(isEditable = false, isPrimaryKey = true)
private Long id;
@QField(possibleValueSourceName = ProcessTrace.TABLE_NAME)
private Long processTraceId;
@QField(label = "Backend", possibleValueSourceName = QQQBackend.TABLE_NAME)
private Integer qqqBackendId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(label = "Action", possibleValueSourceName = ProcessTraceBackendActivityStatsAction.NAME)
private String actionName;
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer callCount;
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer recordCount;
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer runtimeMillis;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public ProcessTraceBackendActivityStats()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public ProcessTraceBackendActivityStats(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
* Getter for id
* @see #withId(Long)
*******************************************************************************/
public Long getId()
{
return (this.id);
}
/*******************************************************************************
* Setter for id
* @see #withId(Long)
*******************************************************************************/
public void setId(Long id)
{
this.id = id;
}
/*******************************************************************************
* Fluent setter for id
*******************************************************************************/
public ProcessTraceBackendActivityStats withId(Long id)
{
this.id = id;
return (this);
}
/*******************************************************************************
* Getter for processTraceId
* @see #withProcessTraceId(Long)
*******************************************************************************/
public Long getProcessTraceId()
{
return (this.processTraceId);
}
/*******************************************************************************
* Setter for processTraceId
* @see #withProcessTraceId(Long)
*******************************************************************************/
public void setProcessTraceId(Long processTraceId)
{
this.processTraceId = processTraceId;
}
/*******************************************************************************
* Fluent setter for processTraceId
*******************************************************************************/
public ProcessTraceBackendActivityStats withProcessTraceId(Long processTraceId)
{
this.processTraceId = processTraceId;
return (this);
}
/*******************************************************************************
* Getter for actionName
* @see #withActionName(String)
*******************************************************************************/
public String getActionName()
{
return (this.actionName);
}
/*******************************************************************************
* Setter for actionName
* @see #withActionName(String)
*******************************************************************************/
public void setActionName(String actionName)
{
this.actionName = actionName;
}
/*******************************************************************************
* Fluent setter for actionName
*******************************************************************************/
public ProcessTraceBackendActivityStats withActionName(String actionName)
{
this.actionName = actionName;
return (this);
}
/*******************************************************************************
* Getter for callCount
* @see #withCallCount(Integer)
*******************************************************************************/
public Integer getCallCount()
{
return (this.callCount);
}
/*******************************************************************************
* Setter for callCount
* @see #withCallCount(Integer)
*******************************************************************************/
public void setCallCount(Integer callCount)
{
this.callCount = callCount;
}
/*******************************************************************************
* Fluent setter for callCount
*******************************************************************************/
public ProcessTraceBackendActivityStats withCallCount(Integer callCount)
{
this.callCount = callCount;
return (this);
}
/*******************************************************************************
* Getter for recordCount
* @see #withRecordCount(Integer)
*******************************************************************************/
public Integer getRecordCount()
{
return (this.recordCount);
}
/*******************************************************************************
* Setter for recordCount
* @see #withRecordCount(Integer)
*******************************************************************************/
public void setRecordCount(Integer recordCount)
{
this.recordCount = recordCount;
}
/*******************************************************************************
* Fluent setter for recordCount
*******************************************************************************/
public ProcessTraceBackendActivityStats withRecordCount(Integer recordCount)
{
this.recordCount = recordCount;
return (this);
}
/*******************************************************************************
* Getter for runtimeMillis
* @see #withRuntimeMillis(Integer)
*******************************************************************************/
public Integer getRuntimeMillis()
{
return (this.runtimeMillis);
}
/*******************************************************************************
* Setter for runtimeMillis
* @see #withRuntimeMillis(Integer)
*******************************************************************************/
public void setRuntimeMillis(Integer runtimeMillis)
{
this.runtimeMillis = runtimeMillis;
}
/*******************************************************************************
* Fluent setter for runtimeMillis
*******************************************************************************/
public ProcessTraceBackendActivityStats withRuntimeMillis(Integer runtimeMillis)
{
this.runtimeMillis = runtimeMillis;
return (this);
}
/*******************************************************************************
* Getter for qqqBackendId
* @see #withQqqBackendId(Integer)
*******************************************************************************/
public Integer getQqqBackendId()
{
return (this.qqqBackendId);
}
/*******************************************************************************
* Setter for qqqBackendId
* @see #withQqqBackendId(Integer)
*******************************************************************************/
public void setQqqBackendId(Integer qqqBackendId)
{
this.qqqBackendId = qqqBackendId;
}
/*******************************************************************************
* Fluent setter for qqqBackendId
*******************************************************************************/
public ProcessTraceBackendActivityStats withQqqBackendId(Integer qqqBackendId)
{
this.qqqBackendId = qqqBackendId;
return (this);
}
/*******************************************************************************
* Getter for qqqTableId
* @see #withQqqTableId(Integer)
*******************************************************************************/
public Integer getQqqTableId()
{
return (this.qqqTableId);
}
/*******************************************************************************
* Setter for qqqTableId
* @see #withQqqTableId(Integer)
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
}
/*******************************************************************************
* Fluent setter for qqqTableId
*******************************************************************************/
public ProcessTraceBackendActivityStats withQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
return (this);
}
}
@@ -0,0 +1,131 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.model;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditDetailAccumulator;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** ProcessTraceBackendActivityStatsAction - possible value enum
*******************************************************************************/
public enum ProcessTraceBackendActivityStatsAction implements PossibleValueEnum<String>
{
INSERT(InsertAction.class.getSimpleName(), "Insert"),
UPDATE(UpdateAction.class.getSimpleName(), "Update"),
DELETE(DeleteAction.class.getSimpleName(), "Delete"),
QUERY(QueryAction.class.getSimpleName(), "Query"),
GET(GetAction.class.getSimpleName(), "Get"),
COUNT(CountAction.class.getSimpleName(), "Count"),
AGGREGATE(AuditDetailAccumulator.class.getSimpleName(), "Aggregate");
private final String id;
private final String label;
public static final String NAME = "ProcessTraceBackendActivityStatsAction";
/*******************************************************************************
**
*******************************************************************************/
ProcessTraceBackendActivityStatsAction(String id, String label)
{
this.id = id;
this.label = label;
}
/*******************************************************************************
** Get instance by id
**
*******************************************************************************/
public static ProcessTraceBackendActivityStatsAction getById(String id)
{
if(id == null)
{
return (null);
}
for(ProcessTraceBackendActivityStatsAction value : ProcessTraceBackendActivityStatsAction.values())
{
if(Objects.equals(value.id, id))
{
return (value);
}
}
return (null);
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public String getId()
{
return id;
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueId()
{
return (getId());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return (getLabel());
}
}
@@ -5,65 +5,38 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.model;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerMessage;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerKeyRecordMessage;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessTraceKeyRecordMessage extends ProcessTracerMessage
@Deprecated(since = "Moved to ProcessTracerKeyRecordMessage in qqq-backend-core")
public class ProcessTraceKeyRecordMessage extends ProcessTracerKeyRecordMessage
{
private final String tableName;
private final Integer recordId;
/***************************************************************************
**
***************************************************************************/
public ProcessTraceKeyRecordMessage(String tableName, Integer recordId)
{
super("Process Key Record is " + tableName + " " + recordId);
this.tableName = tableName;
this.recordId = recordId;
super(tableName, recordId);
}
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return (tableName);
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getRecordId()
{
return (recordId);
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.model;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -24,11 +23,16 @@ package com.kingsrook.qbits.standardprocesstrace.model;
import java.util.List;
import com.kingsrook.qbits.standardprocesstrace.utils.ProcessTraceSummaryLineRecordIntTableCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
@@ -78,6 +82,9 @@ public class ProcessTraceSummaryLineRecordInt extends QRecordEntity
.withExposedJoin(new ExposedJoin().withLabel("Summary Line").withJoinPath(List.of(parentJoinName)).withJoinTable(ProcessTraceSummaryLine.TABLE_NAME))
.withExposedJoin(new ExposedJoin().withLabel("Process Trace").withJoinPath(List.of(parentJoinName, grandparentJoinName)).withJoinTable(ProcessTrace.TABLE_NAME));
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(ProcessTraceSummaryLineRecordIntTableCustomizer.class));
table.getField("recordId").withFieldAdornment(new FieldAdornment(AdornmentType.LINK).withValue(AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, true));
return (table);
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -0,0 +1,297 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.backends.QQQBackendTableManager;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
* Singleton that collects QueryStats, associated with a processTraceId.
*
* <p>Runs a scheduled executor thread to clean itself up - e.g., in case processes
* leak (e.g., started in a frontend but exited before final step). Configured by
* {@link #setCleanupIntervalMillis(long)} and {@link #setCleanupMaxAge(long)}</p>
*******************************************************************************/
public class ProcessTraceBackendActivityStatsManager
{
private static final QLogger LOG = QLogger.getLogger(ProcessTraceBackendActivityStatsManager.class);
private static ProcessTraceBackendActivityStatsManager processTraceBackendActivityStatsManager = null;
private Map<Long, ProcessStats> stats = Collections.synchronizedMap(new HashMap<>());
private long CLEANUP_INTERVAL_MILLIS = 60 * 60 * 1000;
private long CLEANUP_MAX_AGE = 12 * 60 * 60 * 1000;
private ScheduledExecutorService scheduledExecutorService;
/***************************************************************************
* container for all data collected for a particular process trace.
*
* stores start milliseconds (for cleanup), plus map of keys to stats.
***************************************************************************/
private record ProcessStats(long startMillis, Map<Key, Stats> stats)
{
/*******************************************************************************
** Constructor that uses default values
**
*******************************************************************************/
public ProcessStats()
{
this(System.currentTimeMillis(), new LinkedHashMap<>());
}
}
/***************************************************************************
* what backend/table/action a set of stats are associated with.
*
* e.g., memoryBackend, personTable, insertAction
***************************************************************************/
public record Key(String backendName, String tableName, String actionName) implements Serializable
{
}
/***************************************************************************
* the stats collected for individual keys.
***************************************************************************/
private record Stats(Integer calls, Integer records, Integer millis)
{
}
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private ProcessTraceBackendActivityStatsManager()
{
scheduleCleanupJob();
}
/***************************************************************************
*
***************************************************************************/
private void scheduleCleanupJob()
{
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new PrefixedDefaultThreadFactory("ProcessTraceBackendActivityStatsManager-cleanup"));
scheduledExecutorService.scheduleAtFixedRate(this::cleanup, CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static ProcessTraceBackendActivityStatsManager getInstance()
{
if(processTraceBackendActivityStatsManager == null)
{
processTraceBackendActivityStatsManager = new ProcessTraceBackendActivityStatsManager();
}
return (processTraceBackendActivityStatsManager);
}
/***************************************************************************
* shutdown the scheduled job. also clear stats.
***************************************************************************/
public void shutdown()
{
scheduledExecutorService.shutdown();
stats.clear();
}
/***************************************************************************
* initialize storage for stats for a specific process trace id
***************************************************************************/
public void initProcess(Long processTraceId)
{
stats.put(processTraceId, new ProcessStats());
}
/***************************************************************************
* clean up stat entries for processes that were originally created before
* CLEANUP_MAX_AGE (per {@link #setCleanupMaxAge(long)} milliseconds ago.
***************************************************************************/
private void cleanup()
{
long limitStartMillis = System.currentTimeMillis() - CLEANUP_MAX_AGE;
LOG.info("Starting cleanup of ProcessTraceBackendActivityStatsManager", logPair("size", stats.size()));
stats.entrySet().removeIf(entry -> entry.getValue().startMillis < limitStartMillis);
LOG.info("Finished cleanup of ProcessTraceBackendActivityStatsManager", logPair("size", stats.size()));
}
/***************************************************************************
* when storing a processTrace (e.g., after the process has finished running),
* convert the stats stored in this object to a list of {@link ProcessTraceBackendActivityStats}
* entities. Also remove the stats from this object's internal storage.
***************************************************************************/
public List<ProcessTraceBackendActivityStats> getAndRemoveStats(Long processTraceId) throws QException
{
List<ProcessTraceBackendActivityStats> rs = new ArrayList<>();
ProcessStats processStats = stats.remove(processTraceId);
if(processStats != null)
{
for(Map.Entry<Key, Stats> entry : CollectionUtils.nonNullMap(processStats.stats()).entrySet())
{
rs.add(new ProcessTraceBackendActivityStats()
.withProcessTraceId(processTraceId)
.withQqqBackendId(getBackendId(entry.getKey().backendName()))
.withQqqTableId(getTableId(entry.getKey().tableName()))
.withActionName(entry.getKey().actionName())
.withCallCount(entry.getValue().calls())
.withRecordCount(entry.getValue().records())
.withRuntimeMillis(entry.getValue().millis())
);
}
}
return (rs);
}
/***************************************************************************
*
***************************************************************************/
private Integer getBackendId(String backendName)
{
if(!StringUtils.hasContent(backendName))
{
return (null);
}
try
{
return QQQBackendTableManager.getQQQBackendId(QContext.getQInstance(), backendName);
}
catch(Exception e)
{
return (null);
}
}
/***************************************************************************
*
***************************************************************************/
private Integer getTableId(String tableName)
{
if(!StringUtils.hasContent(tableName))
{
return (null);
}
try
{
return QQQTableTableManager.getQQQTableId(QContext.getQInstance(), tableName);
}
catch(Exception e)
{
return (null);
}
}
/***************************************************************************
* increment the stat values for a given process.
*
* @param key the backend, table, and action
* @param addCalls the number of calls to add (typically 1 at a time?)
* @param addRecords the number of records to add
* @param addMillis the number of milliseconds of runtime to add
***************************************************************************/
public void add(Long processTraceId, Key key, Integer addCalls, Integer addRecords, Integer addMillis)
{
ProcessStats processStats = stats.computeIfAbsent(processTraceId, k -> new ProcessStats());
Map<Key, Stats> countsForProcess = processStats.stats();
if(countsForProcess != null)
{
Stats statsForKey = countsForProcess.computeIfAbsent(key, (k) -> new Stats(0, 0, 0));
countsForProcess.put(key, new Stats(statsForKey.calls + addCalls, statsForKey.records + addRecords, statsForKey.millis + addMillis));
}
}
/***************************************************************************
* sets the cleanup job's scheduled interval. Also reschedules the job.
*
* @param cleanupIntervalMillis milliseconds between runs of the cleanup job
***************************************************************************/
public void setCleanupIntervalMillis(long cleanupIntervalMillis)
{
CLEANUP_INTERVAL_MILLIS = cleanupIntervalMillis;
scheduledExecutorService.shutdown();
scheduleCleanupJob();
}
/***************************************************************************
* @param cleanupMaxAge max age (in millis) that a process is allowed to be
* before it gets cleaned up when a cleanup job runs.
***************************************************************************/
public void setCleanupMaxAge(long cleanupMaxAge)
{
CLEANUP_MAX_AGE = cleanupMaxAge;
}
}
@@ -0,0 +1,77 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessTraceSummaryLineRecordIntTableCustomizer implements TableCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
if(queryInput.getShouldGenerateDisplayValues())
{
///////////////////////////////////////////////////////////////////////////////////////
// for records with a qqqTableId - look up that table name, then set a display-value //
// for the Link type adornment, to the recordId record within that table. //
///////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> tableIds = records.stream().map(r -> r.getValue("qqqTableId")).filter(Objects::nonNull).collect(Collectors.toSet());
if(!tableIds.isEmpty())
{
Map<Serializable, QRecord> tableMap = GeneralProcessUtils.loadTableToMap(QQQTable.TABLE_NAME, "id", new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, tableIds)));
for(QRecord record : records)
{
QRecord qqqTableRecord = tableMap.get(record.getValue("qqqTableId"));
if(qqqTableRecord != null && record.getValue("recordId") != null)
{
record.setDisplayValue("recordId:" + AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, qqqTableRecord.getValueString("name"));
}
}
}
}
return (records);
}
}
@@ -0,0 +1,77 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessTraceTableCustomizer implements TableCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
if(queryInput.getShouldGenerateDisplayValues())
{
////////////////////////////////////////////////////////////////////////////////////////////////
// for records with a keyRecordQqqTableId - look up that table name, then set a display-value //
// for the Link type adornment, to the keyRecordId record within that table. //
////////////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> tableIds = records.stream().map(r -> r.getValue("keyRecordQqqTableId")).filter(Objects::nonNull).collect(Collectors.toSet());
if(!tableIds.isEmpty())
{
Map<Serializable, QRecord> tableMap = GeneralProcessUtils.loadTableToMap(QQQTable.TABLE_NAME, "id", new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, tableIds)));
for(QRecord record : records)
{
QRecord qqqTableRecord = tableMap.get(record.getValue("keyRecordQqqTableId"));
if(qqqTableRecord != null && record.getValue("keyRecordId") != null)
{
record.setDisplayValue("keyRecordId:" + AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, qqqTableRecord.getValueString("name"));
}
}
}
}
return (records);
}
}
@@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.RecordListWidgetRenderer;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** factory/builder class, to create instances of RecordListWidgetRenderer for
** specific tables and/or processes within a table.
*******************************************************************************/
public class RecentProcessTracesWidgetBuilder
{
/***************************************************************************
**
***************************************************************************/
public static QWidgetMetaData buildWidget(String tableName)
{
QWidgetMetaData widgetMetaData = RecordListWidgetRenderer.widgetMetaDataBuilder(tableName + "RecentProcessTraces")
.withTableName(ProcessTrace.TABLE_NAME)
.withMaxRows(20)
.withLabel("Recent Process Traces")
.withFilter(new QQueryFilter()
.withCriteria("qqqTable.name", QCriteriaOperator.EQUALS, tableName)
.withCriteria("keyRecordId", QCriteriaOperator.EQUALS, "${input.id}")
.withOrderBy(new QFilterOrderBy("id", false))
).getWidgetMetaData();
return (widgetMetaData);
}
/***************************************************************************
**
***************************************************************************/
public static QWidgetMetaData buildWidget(String tableName, String processName)
{
QWidgetMetaData widgetMetaData = RecordListWidgetRenderer.widgetMetaDataBuilder(tableName + StringUtils.ucFirst(processName) + "RecentProcessTraces")
.withTableName(ProcessTrace.TABLE_NAME)
.withMaxRows(20)
.withLabel("Recent Process Traces")
.withFilter(new QQueryFilter()
.withCriteria("qqqProcess.name", QCriteriaOperator.EQUALS, processName)
.withCriteria("qqqTable.name", QCriteriaOperator.EQUALS, tableName)
.withCriteria("keyRecordId", QCriteriaOperator.EQUALS, "${input.id}")
.withOrderBy(new QFilterOrderBy("id", false))
).getWidgetMetaData();
return (widgetMetaData);
}
}
@@ -0,0 +1,66 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatConsumerInterface;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
/*******************************************************************************
* Standard Process Trace's implementation of a QueryStat consumer.
*
* <p>That is, to accumulate them in the {@link ProcessTraceBackendActivityStatsManager},
* associated with the processTraceId in the user's session, so they can be stored
* when the process trace is stored.</p>
*******************************************************************************/
public class StandardProcessTraceQueryStatConsumer implements QueryStatConsumerInterface
{
private static final QLogger LOG = QLogger.getLogger(StandardProcessTraceQueryStatConsumer.class);
/***************************************************************************
*
***************************************************************************/
@Override
public void accept(QueryStat queryStat)
{
try
{
String processTraceId = QContext.getQSession().getValue(StandardProcessTracer.PROCESS_TRACE_ID_SESSION_KEY);
if(processTraceId == null)
{
return;
}
String backendName = QContext.getQInstance().getBackendForTable(queryStat.getTableName()).getName();
String backendAction = queryStat.getBackendAction();
ProcessTraceBackendActivityStatsManager.Key key = new ProcessTraceBackendActivityStatsManager.Key(backendName, queryStat.getTableName(), backendAction);
ProcessTraceBackendActivityStatsManager.getInstance().add(Long.valueOf(processTraceId), key, 1, queryStat.getRecordCount(), queryStat.getFirstResultMillis());
}
catch(Exception e)
{
LOG.warn("Error accepting query stats for process trace", e);
}
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -30,7 +29,7 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.standardprocesstrace.StandardProcessTraceQBitConfig;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceKeyRecordMessage;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLine;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLineRecordInt;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
@@ -55,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerKeyRecordMessage;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -75,6 +75,8 @@ public class StandardProcessTracer implements ProcessTracerInterface
private static StandardProcessTraceQBitConfig standardProcessTraceQBitConfig;
public static final String PROCESS_TRACE_ID_SESSION_KEY = StandardProcessTracer.class.getName() + ".processTraceId";
private Instant startTime;
private Long processTraceId;
@@ -98,6 +100,17 @@ public class StandardProcessTracer implements ProcessTracerInterface
.withProcessUUID(runProcessInput.getProcessUUID())));
processTraceId = insertOutput.getRecords().get(0).getValueLong("id");
////////////////////////////////////////////////////////////////////////////////////
// store the processTraceId in the session. this will be used by the //
// ProcessTraceQueryStatConsumer to associated QueryStats with this process trace //
////////////////////////////////////////////////////////////////////////////////////
if(QContext.getQSession() != null)
{
QContext.getQSession().setValue(PROCESS_TRACE_ID_SESSION_KEY, String.valueOf(processTraceId));
}
ProcessTraceBackendActivityStatsManager.getInstance().initProcess(processTraceId);
}
catch(QException e)
{
@@ -113,9 +126,29 @@ public class StandardProcessTracer implements ProcessTracerInterface
@Override
public void handleProcessResume(RunProcessInput runProcessInput)
{
/////////////////////////////////
// noop in this implementation //
/////////////////////////////////
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a user is resuming a process, e.g., from a frontend, then we want to put that processTraceId in their session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(processTraceId == null)
{
QRecord processTrace = GetAction.execute(ProcessTrace.TABLE_NAME, Map.of("processUUID", runProcessInput.getProcessUUID()));
if(processTrace != null)
{
processTraceId = processTrace.getValueLong("id");
}
}
if(processTraceId != null && QContext.getQSession() != null)
{
QContext.getQSession().setValue(PROCESS_TRACE_ID_SESSION_KEY, String.valueOf(processTraceId));
}
}
catch(Exception e)
{
LOG.warn("Error resuming process trace", e, logPair("processUUID", () -> runProcessInput.getProcessUUID()));
}
}
@@ -170,7 +203,7 @@ public class StandardProcessTracer implements ProcessTracerInterface
{
try
{
if(message instanceof ProcessTraceKeyRecordMessage keyRecordMessage)
if(message instanceof ProcessTracerKeyRecordMessage keyRecordMessage)
{
Integer keyTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), keyRecordMessage.getTableName());
@@ -297,12 +330,21 @@ public class StandardProcessTracer implements ProcessTracerInterface
for(Serializable primaryKey : processSummaryLine.getPrimaryKeys())
{
Integer recordId = ValueUtils.getValueAsInteger(primaryKey);
if(recordId != null)
try
{
recordIntList.add(new ProcessTraceSummaryLineRecordInt()
.withQqqTableId(qqqTableId)
.withRecordId(recordId));
Integer recordId = ValueUtils.getValueAsInteger(primaryKey);
if(recordId != null)
{
recordIntList.add(new ProcessTraceSummaryLineRecordInt()
.withQqqTableId(qqqTableId)
.withRecordId(recordId));
}
}
catch(Exception e)
{
/////////////////////////////////////////////////////////////////////
// todo log (would be loud)? or, let not be integer maybe better? //
/////////////////////////////////////////////////////////////////////
}
}
}
@@ -319,12 +361,28 @@ public class StandardProcessTracer implements ProcessTracerInterface
new InsertAction().execute(new InsertInput(ProcessTraceSummaryLine.TABLE_NAME).withRecordEntities(summaryLines));
}
//////////////////////////////////////////
// build and store backend stat records //
//////////////////////////////////////////
if(QContext.getQInstance().getTable(ProcessTraceBackendActivityStats.TABLE_NAME) != null)
{
List<ProcessTraceBackendActivityStats> stats = ProcessTraceBackendActivityStatsManager.getInstance().getAndRemoveStats(processTraceId);
new InsertAction().execute(new InsertInput(ProcessTraceBackendActivityStats.TABLE_NAME).withRecordEntities(stats));
}
}
}
catch(Exception e)
{
LOG.warn("Error completing store of processTrace records.", e);
}
finally
{
if(QContext.getQSession() != null)
{
QContext.getQSession().removeValue(PROCESS_TRACE_ID_SESSION_KEY);
}
}
}
@@ -0,0 +1,85 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace;
import java.util.List;
import com.kingsrook.qbits.standardprocesstrace.utils.StandardProcessTraceQueryStatConsumer;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
/*******************************************************************************
* base class for tests for this QBit.
*******************************************************************************/
public class BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void baseBeforeEach() throws Exception
{
QInstance qInstance = defineQInstance();
new QInstanceValidator().validate(qInstance);
QContext.init(qInstance, new QSystemUserSession());
QueryStatManager queryStatManager = QueryStatManager.getInstance();
queryStatManager.start(qInstance, () -> new QSystemUserSession());
queryStatManager.setQueryStatConsumers(List.of(new StandardProcessTraceQueryStatConsumer()));
MemoryRecordStore.fullReset();
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
MemoryRecordStore.fullReset();
QueryStatManager.getInstance().stop();
}
/***************************************************************************
**
***************************************************************************/
private QInstance defineQInstance() throws QException
{
return new StandardProcessTraceTestApplication().defineQInstance();
}
}
@@ -0,0 +1,233 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import com.kingsrook.qbits.standardprocesstrace.utils.StandardProcessTracer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
import com.kingsrook.qqq.backend.core.model.backends.QQQBackendsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.processes.QQQProcessesMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
/*******************************************************************************
* test application (e.g., QInstance) for this QBit's tests.
*******************************************************************************/
public class StandardProcessTraceTestApplication extends AbstractQQQApplication
{
public static final String MEMORY_BACKEND_NAME = "memory";
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_ORDER = "order";
public static final String PROCESS_NAME_PLACE_ORDERS = "placeOrders";
/***************************************************************************
**
***************************************************************************/
@Override
public QInstance defineQInstance() throws QException
{
QInstance qInstance = new QInstance();
qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS));
qInstance.addBackend(new QBackendMetaData()
.withName(MEMORY_BACKEND_NAME)
.withBackendType(MemoryBackendModule.class));
//////////////////////////////
// add tables table and PVS //
//////////////////////////////
new QQQTablesMetaDataProvider().defineAll(qInstance, MEMORY_BACKEND_NAME, MEMORY_BACKEND_NAME, null);
new QQQProcessesMetaDataProvider().defineAll(qInstance, MEMORY_BACKEND_NAME, MEMORY_BACKEND_NAME, null);
new QQQBackendsMetaDataProvider().defineAll(qInstance, MEMORY_BACKEND_NAME, MEMORY_BACKEND_NAME, null);
//////////////////////
// produce our qbit //
//////////////////////
StandardProcessTraceQBitProducer producer = new StandardProcessTraceQBitProducer()
.withStandardProcessTraceQBitConfig(new StandardProcessTraceQBitConfig()
.withTableMetaDataCustomizer((QInstance instance, QTableMetaData table) ->
{
table.setBackendName(MEMORY_BACKEND_NAME);
return (table);
})
.withIncludeBackendActivityStats(true));
MetaDataProducerMultiOutput allQBitMetaData = producer.produce(qInstance);
allQBitMetaData.addSelfToInstance(qInstance);
////////////////////////////////////////
// produce some test tables //
// note - nice to be a qbit this too? //
////////////////////////////////////////
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME_PERSON)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("firstName", QFieldType.STRING).withIsEditable(true))
.withField(new QFieldMetaData("lastName", QFieldType.STRING).withIsEditable(true))
.withCapability(Capability.QUERY_STATS)
);
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME_ORDER)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("orderNo", QFieldType.STRING).withIsEditable(true))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withIsEditable(true))
.withField(new QFieldMetaData("shipTo", QFieldType.STRING).withIsEditable(true))
.withCapability(Capability.QUERY_STATS)
);
qInstance.addProcess(StreamedETLWithFrontendProcess.processMetaDataBuilder()
.withName(PROCESS_NAME_PLACE_ORDERS)
.withTableName(TABLE_NAME_PERSON)
.withSourceTable(TABLE_NAME_PERSON)
.withDestinationTable(TABLE_NAME_ORDER)
.withExtractStepClass(ExtractViaQueryStep.class)
.withTransformStepClass(PlaceOrdersProcessTransformStep.class)
.withLoadStepClass(LoadViaInsertStep.class)
.getProcessMetaData()
.withProcessTracerCodeReference(new QCodeReference(StandardProcessTracer.class))
);
///////////////////////////////////////////
// turn off audits (why on by default??) //
///////////////////////////////////////////
qInstance.getTables().values().forEach(t -> t.setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)));
/////////////////
// create apps //
/////////////////
qInstance.addApp(new QAppMetaData()
.withName("logs")
.withIcon(new QIcon("history_edu"))
.withSection(StandardProcessTraceQBitProducer.getAppSection(qInstance)));
qInstance.addApp(new QAppMetaData()
.withName("testData")
.withIcon(new QIcon("dataset"))
.withSection(new QAppSection()
.withName("people")
.withTables(List.of(TABLE_NAME_PERSON))));
return qInstance;
}
/***************************************************************************
* test process transformer - for each input person, creates an output order (to insert)
***************************************************************************/
public static class PlaceOrdersProcessTransformStep extends AbstractTransformStep
{
private ProcessSummaryLine okLine = new ProcessSummaryLine(Status.OK)
.withMessageSuffix("")
.withSingularFutureMessage("will get a new order")
.withPluralFutureMessage("will get new orders")
.withSingularPastMessage("got a new order")
.withPluralPastMessage("got new orders");
private ProcessSummaryLine errorLine = new ProcessSummaryLine(Status.ERROR)
.withMessageSuffix("")
.withSingularFutureMessage("will not get a new order")
.withPluralFutureMessage("will not get new orders")
.withSingularPastMessage("did not get a new order")
.withPluralPastMessage("did not get new orders");
/***************************************************************************
*
***************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
okLine.addSelfToListIfAnyCount(rs);
errorLine.addSelfToListIfAnyCount(rs);
return rs;
}
/***************************************************************************
*
***************************************************************************/
@Override
public void runOnePage(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
for(QRecord personRecord : runBackendStepInput.getRecords())
{
////////////////////////////////////////
// don't let Mr. Burns order anything //
////////////////////////////////////////
if("Burns".equals(personRecord.getValueString("lastName")))
{
errorLine.incrementCountAndAddPrimaryKey(personRecord.getValue("id"));
continue;
}
QRecord orderRecord = new QRecord()
.withValue("orderNo", UUID.randomUUID().toString())
.withValue("shipTo", personRecord.getValueString("firstName") + " " + personRecord.getValueString("lastName"));
runBackendStepOutput.addRecord(orderRecord);
okLine.incrementCountAndAddPrimaryKey(personRecord.getValue("id"));
}
}
}
}
@@ -0,0 +1,114 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for ProcessTraceBackendActivityStatsManager
*******************************************************************************/
class ProcessTraceBackendActivityStatsManagerTest
{
ProcessTraceBackendActivityStatsManager.Key key = new ProcessTraceBackendActivityStatsManager.Key("b", "t", "a");
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
ProcessTraceBackendActivityStatsManager manager = ProcessTraceBackendActivityStatsManager.getInstance();
manager.shutdown();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMaxAgeLarger() throws QException
{
long processTraceId = 1L;
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(ProcessTraceBackendActivityStatsManager.class);
ProcessTraceBackendActivityStatsManager manager = ProcessTraceBackendActivityStatsManager.getInstance();
manager.initProcess(processTraceId);
manager.add(processTraceId, key, 1, 2, 3);
manager.setCleanupIntervalMillis(10);
manager.setCleanupMaxAge(100);
SleepUtils.sleep(50, TimeUnit.MILLISECONDS);
QLogger.deactivateCollectingLoggerForClass(ProcessTraceBackendActivityStatsManager.class);
assertThat(collectingLogger.getCollectedMessages()).allMatch(clm -> clm.getMessage().contains("\"size\":1"));
List<ProcessTraceBackendActivityStats> stats = manager.getAndRemoveStats(processTraceId);
assertEquals(1, stats.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMaxAgeSmaller() throws QException
{
long processTraceId = 2L;
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(ProcessTraceBackendActivityStatsManager.class);
ProcessTraceBackendActivityStatsManager manager = ProcessTraceBackendActivityStatsManager.getInstance();
manager.initProcess(processTraceId);
manager.add(processTraceId, key, 1, 2, 3);
manager.setCleanupIntervalMillis(5);
manager.setCleanupMaxAge(20);
SleepUtils.sleep(50, TimeUnit.MILLISECONDS);
QLogger.deactivateCollectingLoggerForClass(ProcessTraceBackendActivityStatsManager.class);
assertThat(collectingLogger.getCollectedMessages()).anyMatch(clm -> clm.getMessage().contains("\"size\":1"));
assertThat(collectingLogger.getCollectedMessages()).anyMatch(clm -> clm.getMessage().contains("\"size\":0"));
List<ProcessTraceBackendActivityStats> stats = manager.getAndRemoveStats(processTraceId);
assertEquals(0, stats.size());
////////////////////////////////////////////////////////////////////////////////////////////
// make sure even after a cleanup, we don't fail if we do try to add values for a process //
////////////////////////////////////////////////////////////////////////////////////////////
manager.add(processTraceId, key, 1, 2, 3);
stats = manager.getAndRemoveStats(processTraceId);
assertEquals(1, stats.size());
}
}
@@ -0,0 +1,176 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.standardprocesstrace.utils;
import java.util.List;
import com.kingsrook.qbits.standardprocesstrace.BaseTest;
import com.kingsrook.qbits.standardprocesstrace.StandardProcessTraceTestApplication;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTrace;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceBackendActivityStats;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLine;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryLineRecordInt;
import com.kingsrook.qbits.standardprocesstrace.model.ProcessTraceSummaryStatusEnum;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for StandardProcessTracer
*******************************************************************************/
class StandardProcessTracerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
insertHomerMargeAndMrBurns();
////////////////////////////////////////
// run process skipping backend steps //
////////////////////////////////////////
RunProcessInput input = new RunProcessInput();
input.setProcessName(StandardProcessTraceTestApplication.PROCESS_NAME_PLACE_ORDERS);
input.setCallback(QProcessCallbackFactory.forFilter(new QQueryFilter()));
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
new RunProcessAction().execute(input);
baseAssertionsAfterProcess();
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), StandardProcessTraceTestApplication.TABLE_NAME_PERSON);
Integer orderTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), StandardProcessTraceTestApplication.TABLE_NAME_ORDER);
List<QRecord> processTraceBackendActivityStatRecords = QueryAction.execute(ProcessTraceBackendActivityStats.TABLE_NAME, new QQueryFilter());
assertThat(processTraceBackendActivityStatRecords)
.hasSizeGreaterThanOrEqualTo(2)
.anyMatch(r -> r.getValue("qqqTableId").equals(personTableId) && r.getValue("actionName").equals(QueryAction.class.getSimpleName()) && r.getValueInteger("callCount").equals(1) && r.getValueInteger("recordCount").equals(3))
.anyMatch(r -> r.getValue("qqqTableId").equals(orderTableId) && r.getValue("actionName").equals(InsertAction.class.getSimpleName()) && r.getValueInteger("callCount").equals(1) && r.getValueInteger("recordCount").equals(2));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testResumingFrontendProcess() throws QException
{
insertHomerMargeAndMrBurns();
//////////////////////////////////////
// start process - break at preview //
//////////////////////////////////////
RunProcessInput input = new RunProcessInput();
input.setProcessName(StandardProcessTraceTestApplication.PROCESS_NAME_PLACE_ORDERS);
input.setCallback(QProcessCallbackFactory.forFilter(new QQueryFilter()));
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
RunProcessOutput output = new RunProcessAction().execute(input);
/////////////////////////////////////
// continue to execute the process //
/////////////////////////////////////
input.setProcessUUID(output.getProcessUUID());
input.setStartAfterStep(output.getProcessState().getNextStepName().get());
new RunProcessAction().execute(input);
baseAssertionsAfterProcess();
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), StandardProcessTraceTestApplication.TABLE_NAME_PERSON);
Integer orderTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), StandardProcessTraceTestApplication.TABLE_NAME_ORDER);
List<QRecord> processTraceBackendActivityStatRecords = QueryAction.execute(ProcessTraceBackendActivityStats.TABLE_NAME, new QQueryFilter());
assertThat(processTraceBackendActivityStatRecords)
.hasSizeGreaterThanOrEqualTo(2)
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this time, there'll be 2 calls to the query (for preview, and execute steps), for twice as many records total //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
.anyMatch(r -> r.getValue("qqqTableId").equals(personTableId) && r.getValue("actionName").equals(QueryAction.class.getSimpleName()) && r.getValueInteger("callCount").equals(2) && r.getValueInteger("recordCount").equals(6))
.anyMatch(r -> r.getValue("qqqTableId").equals(orderTableId) && r.getValue("actionName").equals(InsertAction.class.getSimpleName()) && r.getValueInteger("callCount").equals(1) && r.getValueInteger("recordCount").equals(2));
}
/***************************************************************************
*
***************************************************************************/
private static void insertHomerMargeAndMrBurns() throws QException
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// insert 3 people to run through the process. //
// the process will insert an order for Homer & Marge, but is hard-coded to make an error for Mr. Burns. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
new InsertAction().execute(new InsertInput(StandardProcessTraceTestApplication.TABLE_NAME_PERSON).withRecords(List.of(
new QRecord().withValue("id", 100).withValue("firstName", "Homer").withValue("lastName", "Simpson"),
new QRecord().withValue("id", 101).withValue("firstName", "Marge").withValue("lastName", "Simpson"),
new QRecord().withValue("id", 102).withValue("firstName", "Mr.").withValue("lastName", "Burns")
)));
}
/***************************************************************************
*
***************************************************************************/
private static void baseAssertionsAfterProcess() throws QException
{
List<QRecord> processTraceRecords = QueryAction.execute(ProcessTrace.TABLE_NAME, new QQueryFilter());
assertEquals(1, processTraceRecords.size());
List<QRecord> processTraceSummaryLineRecords = QueryAction.execute(ProcessTraceSummaryLine.TABLE_NAME, new QQueryFilter());
assertEquals(2, processTraceSummaryLineRecords.size());
QRecord okSummaryLineRecord = processTraceSummaryLineRecords.stream().filter(r -> r.getValueString("status").equals(ProcessTraceSummaryStatusEnum.OK.getPossibleValueId())).findFirst().get();
assertNotNull(okSummaryLineRecord);
assertEquals(2, okSummaryLineRecord.getValueInteger("recordCount"));
List<QRecord> okRecordInts = QueryAction.execute(ProcessTraceSummaryLineRecordInt.TABLE_NAME, new QQueryFilter(new QFilterCriteria("processTraceSummaryLineId", QCriteriaOperator.EQUALS, okSummaryLineRecord.getValueLong("id"))));
assertEquals(2, okRecordInts.size());
assertThat(okRecordInts).allMatch(r -> r.getValueInteger("recordId").equals(100) || r.getValueInteger("recordId").equals(101));
QRecord errorSummaryLineRecord = processTraceSummaryLineRecords.stream().filter(r -> r.getValueString("status").equals(ProcessTraceSummaryStatusEnum.ERROR.getPossibleValueId())).findFirst().get();
assertNotNull(errorSummaryLineRecord);
assertEquals(1, errorSummaryLineRecord.getValueInteger("recordCount"));
List<QRecord> errorRecordInts = QueryAction.execute(ProcessTraceSummaryLineRecordInt.TABLE_NAME, new QQueryFilter(new QFilterCriteria("processTraceSummaryLineId", QCriteriaOperator.EQUALS, errorSummaryLineRecord.getValueLong("id"))));
assertEquals(1, errorRecordInts.size());
assertEquals(102, errorRecordInts.get(0).getValueInteger("recordId"));
}
}