Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve telemetry with deeper insights into system usage #22337

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a75527d
Filter by active users
licitdev Apr 26, 2024
784a323
Allow simple where clause
licitdev Apr 26, 2024
b29ff66
Update tests
licitdev Apr 26, 2024
c5a94f7
Add field counts
licitdev Apr 29, 2024
23192da
Add database size
licitdev Apr 29, 2024
3bc9a61
Fix test
licitdev Apr 29, 2024
4c9a7b2
Add changeset
licitdev Apr 29, 2024
4e8f6ae
Merge branch 'main' into improve-telemetry
licitdev Apr 29, 2024
502b1b0
Remove comment
licitdev May 3, 2024
e00cff7
Fix user counts for edge cases
licitdev May 3, 2024
bd44bba
Update test
licitdev May 3, 2024
b4645f7
Add db size unit and jsdoc
licitdev May 9, 2024
3b45bf9
Return null if database size undetermined
licitdev May 23, 2024
a888782
Merge branch 'main' into improve-telemetry
licitdev May 23, 2024
1ee6bf0
Fix extensions bundle count
licitdev May 25, 2024
6428c8c
Update tests
licitdev May 25, 2024
bf12ce3
Account for partially enabled bundles
licitdev May 25, 2024
14dc43a
Update tests
licitdev May 25, 2024
afad182
Remove inner await
licitdev May 27, 2024
f225737
Account for bundle enabled with all nested extensions disabled
licitdev May 27, 2024
b306996
Update test
licitdev May 27, 2024
d615b3b
Prefix with users instead
licitdev May 27, 2024
63c989a
Fix invalid activeTotal value
licitdev May 28, 2024
b78511a
redo extension counting and fix test
DanielBiegler May 28, 2024
7c45ff4
shorten get field count
DanielBiegler May 28, 2024
3121cc9
change to test the subtraction instead of implementation
DanielBiegler May 28, 2024
1b50194
use new name for key
DanielBiegler May 28, 2024
dcf8332
Revert users naming convention change
licitdev May 28, 2024
6130381
Merge remote-tracking branch 'origin/main' into improve-telemetry
DanielBiegler May 29, 2024
ccc7608
Get extensions count from ExtensionManager
licitdev May 31, 2024
708eeb5
Update extension count test
licitdev May 31, 2024
93b21eb
Mock EMAIL_TEMPLATES_PATH
licitdev May 31, 2024
974da3b
Fix formatting
licitdev May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fluffy-zebras-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

Check warning on line 1 in .changeset/fluffy-zebras-buy.md

View workflow job for this annotation

GitHub Actions / Lint

File ignored by default.
'@directus/api': patch
---

Improved telemetry with deeper insights into system usage
15 changes: 15 additions & 0 deletions api/src/database/helpers/schema/dialects/cockroachdb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { KNEX_TYPES } from '@directus/constants';
import type { Options } from '../types.js';
import { SchemaHelper } from '../types.js';
import { useEnv } from '@directus/env';

const env = useEnv();

export class SchemaHelperCockroachDb extends SchemaHelper {
override async changeToType(
Expand All @@ -23,4 +26,16 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
return existingName + suffix;
}
}

override async getDatabaseSize(): Promise<number> {
try {
const result = await this.knex
.select(this.knex.raw('round(SUM(range_size_mb) * 1024 * 1024, 0) AS size'))
.from(this.knex.raw('[SHOW RANGES FROM database ??]', [env['DB_DATABASE']]));

return result[0]?.['size'] ? Number(result[0]?.['size']) : 0;
} catch {
return 0;
}
}
}
10 changes: 10 additions & 0 deletions api/src/database/helpers/schema/dialects/mssql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@ export class SchemaHelperMSSQL extends SchemaHelper {
override formatUUID(uuid: string): string {
return uuid.toUpperCase();
}

override async getDatabaseSize(): Promise<number> {
try {
const result = await this.knex.raw('SELECT SUM(size) * 8192 AS size FROM sys.database_files;');

return result[0]?.['size'] ? Number(result[0]?.['size']) : 0;
} catch {
return 0;
}
}
}
21 changes: 21 additions & 0 deletions api/src/database/helpers/schema/dialects/mysql.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useEnv } from '@directus/env';
import type { Knex } from 'knex';
import { getDatabaseVersion } from '../../../index.js';
import { SchemaHelper } from '../types.js';

const env = useEnv();

export class SchemaHelperMySQL extends SchemaHelper {
override applyMultiRelationalSort(
knex: Knex,
Expand All @@ -28,4 +31,22 @@ export class SchemaHelperMySQL extends SchemaHelper {

return super.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
}

override async getDatabaseSize(): Promise<number> {
try {
const result = (await this.knex
.sum('size AS size')
.from(
this.knex
.select(this.knex.raw('data_length + index_length AS size'))
.from('information_schema.TABLES')
.where('table_schema', '=', String(env['DB_DATABASE']))
.as('size'),
)) as Record<string, any>[];

return result[0]?.['size'] ? Number(result[0]?.['size']) : 0;
} catch {
return 0;
}
}
}
10 changes: 10 additions & 0 deletions api/src/database/helpers/schema/dialects/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,14 @@ export class SchemaHelperOracle extends SchemaHelper {

return field.type;
}

override async getDatabaseSize(): Promise<number> {
try {
const result = await this.knex.raw('select SUM(bytes) from dba_segments');

return result[0]?.['SUM(BYTES)'] ? Number(result[0]?.['SUM(BYTES)']) : 0;
} catch {
return 0;
}
}
}
16 changes: 16 additions & 0 deletions api/src/database/helpers/schema/dialects/postgres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEnv } from '@directus/env';
import { SchemaHelper } from '../types.js';

const env = useEnv();

export class SchemaHelperPostgres extends SchemaHelper {
override async getDatabaseSize(): Promise<number> {
try {
const result = await this.knex.select(this.knex.raw(`pg_database_size(?) as size;`, [env['DB_DATABASE']]));

return result[0]?.['size'] ? Number(result[0]?.['size']) : 0;
} catch {
return 0;
}
}
}
12 changes: 12 additions & 0 deletions api/src/database/helpers/schema/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ export class SchemaHelperSQLite extends SchemaHelper {
override async postColumnChange(): Promise<void> {
await this.knex.raw('PRAGMA foreign_keys = ON');
}

override async getDatabaseSize(): Promise<number> {
try {
const result = await this.knex.raw(
'SELECT page_count * page_size as "size" FROM pragma_page_count(), pragma_page_size();',
);

return result[0]?.['size'] ? Number(result[0]?.['size']) : 0;
} catch {
return 0;
}
}
}
6 changes: 3 additions & 3 deletions api/src/database/helpers/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { SchemaHelperDefault as postgres } from './dialects/default.js';
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
export { SchemaHelperDefault as redshift } from './dialects/default.js';
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
4 changes: 4 additions & 0 deletions api/src/database/helpers/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,8 @@ export abstract class SchemaHelper extends DatabaseHelper {
formatUUID(uuid: string): string {
return uuid; // no-op by default
}

async getDatabaseSize(): Promise<number> {
return 0;
paescuj marked this conversation as resolved.
Show resolved Hide resolved
}
licitdev marked this conversation as resolved.
Show resolved Hide resolved
}
35 changes: 29 additions & 6 deletions api/src/telemetry/lib/get-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ import { version } from 'directus/version';
import { type Knex } from 'knex';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { getDatabase, getDatabaseClient } from '../../database/index.js';
import { getFieldCount, type FieldCount } from '../utils/get-field-count.js';
import { getItemCount } from '../utils/get-item-count.js';
import { getUserCount, type UserCount } from '../utils/get-user-count.js';
import { getUserItemCount, type UserItemCount } from '../utils/get-user-item-count.js';
import { getReport } from './get-report.js';

vi.mock('../../database/index.js');

vi.mock('../../database/helpers/index.js', () => ({
getHelpers: vi.fn().mockImplementation(() => ({
schema: {
getDatabaseSize: vi.fn().mockReturnValue(0),
},
})),
}));

// This is required because logger uses global env which is imported before the tests run. Can be
// reduce to just mock the file when logger is also using useLogger everywhere @TODO
vi.mock('@directus/env', () => ({ useEnv: vi.fn().mockReturnValue({}) }));
Expand All @@ -18,11 +27,13 @@ vi.mock('../utils/get-item-count.js');
vi.mock('../utils/get-storage.js');
vi.mock('../utils/get-user-item-count.js');
vi.mock('../utils/get-user-count.js');
vi.mock('../utils/get-field-count.js');

let mockEnv: Record<string, unknown>;
let mockDb: Knex;
let mockUserCounts: UserCount;
let mockUserItemCounts: UserItemCount;
let mockFieldCounts: FieldCount;

beforeEach(() => {
mockEnv = {
Expand All @@ -35,12 +46,15 @@ beforeEach(() => {

mockUserItemCounts = { collections: 25, items: 15000 };

mockFieldCounts = { max: 28, total: 88 };

vi.mocked(useEnv).mockReturnValue(mockEnv);
vi.mocked(getDatabase).mockReturnValue(mockDb);

vi.mocked(getItemCount).mockResolvedValue({});
vi.mocked(getUserCount).mockResolvedValue(mockUserCounts);
vi.mocked(getUserItemCount).mockResolvedValue(mockUserItemCounts);
vi.mocked(getFieldCount).mockResolvedValue(mockFieldCounts);
});

afterEach(() => {
Expand Down Expand Up @@ -72,12 +86,12 @@ test('Runs and returns basic counts', async () => {
const report = await getReport();

expect(getItemCount).toHaveBeenCalledWith(mockDb, [
'directus_dashboards',
'directus_extensions',
'directus_files',
'directus_flows',
'directus_roles',
'directus_shares',
{ collection: 'directus_dashboards' },
{ collection: 'directus_extensions', where: ['enabled', '=', true] },
licitdev marked this conversation as resolved.
Show resolved Hide resolved
{ collection: 'directus_files' },
{ collection: 'directus_flows', where: ['status', '=', 'active'] },
{ collection: 'directus_roles' },
{ collection: 'directus_shares' },
]);

expect(report.dashboards).toBe(mockItemCount.directus_dashboards);
Expand Down Expand Up @@ -106,3 +120,12 @@ test('Runs and returns user item counts', async () => {
expect(report.collections).toBe(mockUserItemCounts.collections);
expect(report.items).toBe(mockUserItemCounts.items);
});

test('Runs and returns field counts', async () => {
const report = await getReport();

expect(getFieldCount).toHaveBeenCalledWith(mockDb);

expect(report.collections).toBe(mockUserItemCounts.collections);
expect(report.items).toBe(mockUserItemCounts.items);
});
34 changes: 25 additions & 9 deletions api/src/telemetry/lib/get-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ import { useEnv } from '@directus/env';
import { version } from 'directus/version';
import { getDatabase, getDatabaseClient } from '../../database/index.js';
import type { TelemetryReport } from '../types/report.js';
import { getFieldCount } from '../utils/get-field-count.js';
import { getItemCount } from '../utils/get-item-count.js';
import { getUserCount } from '../utils/get-user-count.js';
import { getUserItemCount } from '../utils/get-user-item-count.js';
import { getHelpers } from '../../database/helpers/index.js';

const basicCountCollections = [
'directus_dashboards',
'directus_extensions',
'directus_files',
'directus_flows',
'directus_roles',
'directus_shares',
const basicCountTasks = [
{ collection: 'directus_dashboards' },
{
collection: 'directus_extensions',
where: ['enabled', '=', true],
},
{ collection: 'directus_files' },
{
collection: 'directus_flows',
where: ['status', '=', 'active'],
},
{ collection: 'directus_roles' },
{ collection: 'directus_shares' },
] as const;

/**
Expand All @@ -21,11 +29,14 @@ const basicCountCollections = [
export const getReport = async (): Promise<TelemetryReport> => {
const db = getDatabase();
const env = useEnv();
const helpers = getHelpers(db);

const [basicCounts, userCounts, userItemCount] = await Promise.all([
getItemCount(db, basicCountCollections),
const [basicCounts, userCounts, userItemCount, fieldsCounts, databaseSize] = await Promise.all([
getItemCount(db, basicCountTasks),
getUserCount(db),
getUserItemCount(db),
getFieldCount(db),
helpers.schema.getDatabaseSize(),
]);

return {
Expand All @@ -46,5 +57,10 @@ export const getReport = async (): Promise<TelemetryReport> => {

collections: userItemCount.collections,
items: userItemCount.items,

fields_max: fieldsCounts.max,
fields_total: fieldsCounts.total,

database_size: databaseSize,
};
};
15 changes: 15 additions & 0 deletions api/src/telemetry/types/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,19 @@ export interface TelemetryReport {
* Number of shares in the system
*/
shares: number;

/**
* Maximum number of fields in a collection
*/
fields_max: number;

/**
* Number of fields in the system
*/
fields_total: number;

/**
* Size of the database
*/
database_size: number;
licitdev marked this conversation as resolved.
Show resolved Hide resolved
}
30 changes: 30 additions & 0 deletions api/src/telemetry/utils/get-field-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type Knex } from 'knex';

export interface FieldCount {
max: number;
total: number;
}

export const getFieldCount = async (db: Knex): Promise<FieldCount> => {
const counts: FieldCount = {
max: 0,
total: 0,
};

const result = <{ max: number | string; total: number | string }[]>(
await db
.max({ max: 'field_count' })
.sum({ total: 'field_count' })
.from(db.select('collection').count('* as field_count').from('directus_fields').groupBy('collection').as('inner'))
);

if (result[0]?.max) {
counts.max = Number(result[0].max);
}

if (result[0]?.total) {
counts.total = Number(result[0].total);
}
DanielBiegler marked this conversation as resolved.
Show resolved Hide resolved

return counts;
};