Skip to content

Commit cfe52ca

Browse files
Merge pull request #300 from Canner/feature/remote-activity-log
Feature(CORE): Activity log extension
2 parents 23aa7df + ef7921f commit cfe52ca

File tree

21 files changed

+835
-13
lines changed

21 files changed

+835
-13
lines changed

packages/core/src/containers/modules/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ExtensionLoader } from '../../lib/extension-loader';
33
import { ICoreOptions } from '../../models/coreOptions';
44
import templateEngineModules from '../../lib/template-engine/built-in-extensions';
55
import validatorModule from '../../lib/validators/built-in-validators';
6+
import LoggerModule from '../../lib/loggers';
67
import {
78
builtInCodeLoader,
89
builtInTemplateProvider,
@@ -23,6 +24,7 @@ export const extensionModule = (options: ICoreOptions) =>
2324
for (const templateEngineModule of templateEngineModules) {
2425
loader.loadInternalExtensionModule(templateEngineModule);
2526
}
27+
loader.loadInternalExtensionModule(LoggerModule);
2628
// Validator (single module)
2729
loader.loadInternalExtensionModule(validatorModule);
2830
// Template provider (single module)

packages/core/src/containers/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ export const TYPES = {
5252
Extension_CompilerLoader: Symbol.for('Extension_CompilerLoader'),
5353
Extension_DataSource: Symbol.for('Extension_DataSource'),
5454
Extension_ProfileReader: Symbol.for('ProfileReader'),
55+
// Logger
56+
Extension_ActivityLogger: Symbol.for('Extension_ActivityLogger'),
5557
};

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './lib/utils';
22
export * from './lib/validators';
3+
export * from './lib/loggers';
34
export * from './lib/template-engine';
45
export * from './lib/artifact-builder';
56
export * from './lib/data-query';

packages/core/src/lib/cache-layer/cacheLayerLoader.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export class CacheLayerLoader implements ICacheLayerLoader {
2222
private options: ICacheLayerOptions;
2323
private cacheStorage: DataSource;
2424
private logger = getLogger({ scopeName: 'CORE' });
25-
2625
constructor(
2726
@inject(TYPES.CacheLayerOptions) options: CacheLayerOptions,
2827
@inject(TYPES.Factory_DataSource)

packages/core/src/lib/cache-layer/cacheLayerRefresher.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import ms, { StringValue } from 'ms';
22
import { uniq } from 'lodash';
33
import { ToadScheduler, SimpleIntervalJob, AsyncTask } from 'toad-scheduler';
4-
import { inject, injectable } from 'inversify';
4+
import { inject, injectable, multiInject } from 'inversify';
55
import { TYPES } from '@vulcan-sql/core/types';
6-
import { APISchema } from '@vulcan-sql/core/models';
6+
import {
7+
APISchema,
8+
ActivityLogContentOptions,
9+
ActivityLogType,
10+
CacheLayerInfo,
11+
IActivityLogger,
12+
} from '@vulcan-sql/core/models';
713
import { ConfigurationError } from '../utils/errors';
814
import { ICacheLayerLoader } from './cacheLayerLoader';
15+
import { getLogger } from '../utils';
16+
import moment = require('moment');
917

18+
enum RefreshResult {
19+
SUCCESS = 'SUCCESS',
20+
FAILED = 'FAILED',
21+
}
1022
export interface ICacheLayerRefresher {
1123
/**
1224
* Start the job to load the data source to cache storage and created tables from cache settings in schemas
@@ -22,9 +34,16 @@ export interface ICacheLayerRefresher {
2234
export class CacheLayerRefresher implements ICacheLayerRefresher {
2335
private cacheLoader: ICacheLayerLoader;
2436
private scheduler = new ToadScheduler();
37+
private activityLoggers: IActivityLogger[];
38+
private logger = getLogger({ scopeName: 'CORE' });
2539

26-
constructor(@inject(TYPES.CacheLayerLoader) loader: ICacheLayerLoader) {
40+
constructor(
41+
@inject(TYPES.CacheLayerLoader) loader: ICacheLayerLoader,
42+
@multiInject(TYPES.Extension_ActivityLogger)
43+
activityLoggers: IActivityLogger[]
44+
) {
2745
this.cacheLoader = loader;
46+
this.activityLoggers = activityLoggers;
2847
}
2948

3049
public async start(
@@ -53,16 +72,14 @@ export class CacheLayerRefresher implements ICacheLayerRefresher {
5372
const refreshJob = new SimpleIntervalJob(
5473
{ milliseconds, runImmediately },
5574
new AsyncTask(workerId, async () => {
56-
// load data the to cache storage
57-
58-
await this.cacheLoader.load(templateName, cache);
75+
await this.loadCacheAndSendActivityLog(schema, cache);
5976
}),
6077
{ preventOverrun: true, id: workerId }
6178
);
6279
// add the job to schedule cache refresh task
6380
this.scheduler.addIntervalJob(refreshJob);
6481
} else {
65-
await this.cacheLoader.load(templateName, cache);
82+
await this.loadCacheAndSendActivityLog(schema, cache);
6683
}
6784
})
6885
);
@@ -77,6 +94,44 @@ export class CacheLayerRefresher implements ICacheLayerRefresher {
7794
this.scheduler.stop();
7895
}
7996

97+
private async loadCacheAndSendActivityLog(
98+
schema: APISchema,
99+
cache: CacheLayerInfo
100+
) {
101+
const { urlPath } = schema;
102+
const { sql } = cache;
103+
let refreshResult = RefreshResult.SUCCESS;
104+
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
105+
const templateName = schema.templateSource.replace('/', '_');
106+
try {
107+
// get the current time in format of UTC
108+
await this.cacheLoader.load(templateName, cache);
109+
} catch (error: any) {
110+
refreshResult = RefreshResult.FAILED;
111+
this.logger.debug(`Failed to refresh cache: ${error}`);
112+
} finally {
113+
// send activity log
114+
const content = {
115+
isSuccess: refreshResult === RefreshResult.SUCCESS ? true : false,
116+
activityLogType: ActivityLogType.CACHE_REFRESH,
117+
logTime: now,
118+
urlPath,
119+
sql,
120+
} as ActivityLogContentOptions;
121+
const activityLoggers = this.getActivityLoggers();
122+
for (const activityLogger of activityLoggers)
123+
activityLogger.log(content).catch((err: any) => {
124+
this.logger.debug(
125+
`Failed to log activity after refreshing cache: ${err}`
126+
);
127+
});
128+
}
129+
}
130+
131+
private getActivityLoggers(): IActivityLogger[] {
132+
return this.activityLoggers.filter((logger) => logger.isEnabled());
133+
}
134+
80135
private checkDuplicateCacheTableName(schemas: APISchema[]) {
81136
const tableNames = schemas
82137
// => [[table1, table2], [table1, table3], [table4]]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
BaseActivityLogger,
3+
ActivityLoggerType,
4+
} from '../../models/extensions/logger';
5+
import {
6+
VulcanExtensionId,
7+
VulcanInternalExtension,
8+
} from '../../models/extensions';
9+
import axios, { AxiosRequestHeaders } from 'axios';
10+
import { ConnectionConfig, getUrl } from '../utils/url';
11+
12+
export interface HttpLoggerConfig {
13+
connection?: HttpLoggerConnectionConfig;
14+
}
15+
16+
export interface HttpLoggerConnectionConfig extends ConnectionConfig {
17+
headers?: Record<string, string | number | boolean> | undefined;
18+
}
19+
20+
@VulcanInternalExtension('activity-log')
21+
@VulcanExtensionId(ActivityLoggerType.HTTP_LOGGER)
22+
export class HttpLogger extends BaseActivityLogger<HttpLoggerConfig> {
23+
private logger = this.getLogger();
24+
25+
public async log(payload: any): Promise<void> {
26+
if (!this.isEnabled()) return;
27+
const option = this.getOptions();
28+
if (!option?.connection) {
29+
throw new Error('Http logger connection should be provided');
30+
}
31+
const headers = option.connection.headers;
32+
const url = getUrl(option.connection);
33+
try {
34+
// get connection info from option and use axios to send a post requet to the endpoint
35+
await this.sendActivityLog(url, payload, headers);
36+
this.logger.debug(`Activity log sent`);
37+
} catch (err) {
38+
this.logger.debug(
39+
`Failed to send activity log to http logger, url: ${url}`
40+
);
41+
throw err;
42+
}
43+
}
44+
45+
protected async sendActivityLog(
46+
url: string,
47+
payload: any,
48+
headers: AxiosRequestHeaders | undefined
49+
): Promise<void> {
50+
await axios.post(url, payload, {
51+
headers: headers,
52+
});
53+
}
54+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { HttpLogger } from './httpLogger';
2+
export * from './httpLogger';
3+
4+
export default [HttpLogger];

packages/core/src/lib/template-engine/compiler-environment/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as nunjucks from 'nunjucks';
77
export abstract class BaseCompilerEnvironment extends nunjucks.Environment {
88
abstract getExtensions(): ExtensionBase[];
99

10+
// initialize template engines extensions
1011
public async initializeExtensions() {
1112
const extensions = this.getExtensions();
1213
for (const extension of extensions) {

packages/core/src/lib/utils/url.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export interface ConnectionConfig {
2+
ssl?: boolean;
3+
host?: string;
4+
port?: number | string;
5+
path?: string;
6+
}
7+
8+
export const getUrl = (connection: ConnectionConfig): string => {
9+
const { ssl, host, port, path = '' } = connection;
10+
const protocol = ssl ? 'https' : 'http';
11+
let urlbase = `${protocol}://${host}`;
12+
urlbase = port ? `${urlbase}:${port}` : urlbase;
13+
return new URL(path, urlbase).href;
14+
};

packages/core/src/models/coreOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IArtifactBuilderOptions } from './artifactBuilderOptions';
22
import { ICacheLayerOptions } from './cacheLayerOptions';
33
import { IDocumentOptions } from './documentOptions';
4+
import { IActivityLoggerOptions } from './loggerOptions';
45
import { IProfilesLookupOptions } from './profilesLookupOptions';
56
import { ITemplateEngineOptions } from './templateEngineOptions';
67

@@ -24,6 +25,7 @@ export interface ICoreOptions {
2425
extensions?: ExtensionAliases;
2526
document?: IDocumentOptions;
2627
profiles?: IProfilesLookupOptions;
28+
'activity-log'?: IActivityLoggerOptions;
2729
cache?: ICacheLayerOptions;
2830
[moduleAlias: string]: any;
2931
}

0 commit comments

Comments
 (0)