Browse Source

feat(core): Enhance cache service and SQL cache strategy with tracing support

David Höck 9 months ago
parent
commit
374db4232a

+ 34 - 1
packages/core/src/cache/cache.service.ts

@@ -1,5 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
+import { Span, TraceService } from 'nestjs-otel';
 
 
 import { ConfigService } from '../config/config.service';
 import { ConfigService } from '../config/config.service';
 import { Logger } from '../config/index';
 import { Logger } from '../config/index';
@@ -20,7 +21,10 @@ import { Cache, CacheConfig } from './cache';
 @Injectable()
 @Injectable()
 export class CacheService {
 export class CacheService {
     protected cacheStrategy: CacheStrategy;
     protected cacheStrategy: CacheStrategy;
-    constructor(private configService: ConfigService) {
+    constructor(
+        private configService: ConfigService,
+        private traceService: TraceService,
+    ) {
         this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
         this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
     }
     }
 
 
@@ -31,7 +35,10 @@ export class CacheService {
      * The `Cache` instance provides a convenience wrapper around the `CacheService`
      * The `Cache` instance provides a convenience wrapper around the `CacheService`
      * methods.
      * methods.
      */
      */
+    @Span('vendure.cache.create-cache')
     createCache(config: CacheConfig): Cache {
     createCache(config: CacheConfig): Cache {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.config', JSON.stringify(config));
         return new Cache(config, this);
         return new Cache(config, this);
     }
     }
 
 
@@ -40,12 +47,17 @@ export class CacheService {
      * Gets an item from the cache, or returns undefined if the key is not found, or the
      * Gets an item from the cache, or returns undefined if the key is not found, or the
      * item has expired.
      * item has expired.
      */
      */
+    @Span('vendure.cache.get')
     async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
     async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.key', key);
         try {
         try {
             const result = await this.cacheStrategy.get(key);
             const result = await this.cacheStrategy.get(key);
             if (result) {
             if (result) {
+                span?.setAttribute('cache.hit', true);
                 Logger.debug(`CacheService hit for key [${key}]`);
                 Logger.debug(`CacheService hit for key [${key}]`);
             }
             }
+            span?.end();
             return result as T;
             return result as T;
         } catch (e: any) {
         } catch (e: any) {
             Logger.error(`Could not get key [${key}] from CacheService`, undefined, e.stack);
             Logger.error(`Could not get key [${key}] from CacheService`, undefined, e.stack);
@@ -60,15 +72,22 @@ export class CacheService {
      * Optionally a "time to live" (ttl) can be specified, which means that the key will
      * Optionally a "time to live" (ttl) can be specified, which means that the key will
      * be considered stale after that many milliseconds.
      * be considered stale after that many milliseconds.
      */
      */
+    @Span('vendure.cache.set')
     async set<T extends JsonCompatible<T>>(
     async set<T extends JsonCompatible<T>>(
         key: string,
         key: string,
         value: T,
         value: T,
         options?: SetCacheKeyOptions,
         options?: SetCacheKeyOptions,
     ): Promise<void> {
     ): Promise<void> {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.key', key);
         try {
         try {
             await this.cacheStrategy.set(key, value, options);
             await this.cacheStrategy.set(key, value, options);
+            span?.setAttribute('cache.set', true);
+            span?.end();
             Logger.debug(`Set key [${key}] in CacheService`);
             Logger.debug(`Set key [${key}] in CacheService`);
         } catch (e: any) {
         } catch (e: any) {
+            span?.setAttribute('cache.set', false);
+            span?.end();
             Logger.error(`Could not set key [${key}] in CacheService`, undefined, e.stack);
             Logger.error(`Could not set key [${key}] in CacheService`, undefined, e.stack);
         }
         }
     }
     }
@@ -77,11 +96,18 @@ export class CacheService {
      * @description
      * @description
      * Deletes an item from the cache.
      * Deletes an item from the cache.
      */
      */
+    @Span('vendure.cache.delete')
     async delete(key: string): Promise<void> {
     async delete(key: string): Promise<void> {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.key', key);
         try {
         try {
             await this.cacheStrategy.delete(key);
             await this.cacheStrategy.delete(key);
+            span?.setAttribute('cache.deleted', true);
+            span?.end();
             Logger.debug(`Deleted key [${key}] from CacheService`);
             Logger.debug(`Deleted key [${key}] from CacheService`);
         } catch (e: any) {
         } catch (e: any) {
+            span?.setAttribute('cache.deleted', false);
+            span?.end();
             Logger.error(`Could not delete key [${key}] from CacheService`, undefined, e.stack);
             Logger.error(`Could not delete key [${key}] from CacheService`, undefined, e.stack);
         }
         }
     }
     }
@@ -90,11 +116,18 @@ export class CacheService {
      * @description
      * @description
      * Deletes all items from the cache which contain at least one matching tag.
      * Deletes all items from the cache which contain at least one matching tag.
      */
      */
+    @Span('vendure.cache.invalidate-tags')
     async invalidateTags(tags: string[]): Promise<void> {
     async invalidateTags(tags: string[]): Promise<void> {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.tags', tags.join(', '));
         try {
         try {
             await this.cacheStrategy.invalidateTags(tags);
             await this.cacheStrategy.invalidateTags(tags);
+            span?.setAttribute('cache.invalidated', true);
+            span?.end();
             Logger.debug(`Invalidated tags [${tags.join(', ')}] from CacheService`);
             Logger.debug(`Invalidated tags [${tags.join(', ')}] from CacheService`);
         } catch (e: any) {
         } catch (e: any) {
+            span?.setAttribute('cache.invalidated', false);
+            span?.end();
             Logger.error(
             Logger.error(
                 `Could not invalidate tags [${tags.join(', ')}] from CacheService`,
                 `Could not invalidate tags [${tags.join(', ')}] from CacheService`,
                 undefined,
                 undefined,

+ 15 - 0
packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts

@@ -1,4 +1,5 @@
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
+import { Span, TraceService } from 'nestjs-otel';
 
 
 import { CacheTtlProvider, DefaultCacheTtlProvider } from '../../cache/cache-ttl-provider';
 import { CacheTtlProvider, DefaultCacheTtlProvider } from '../../cache/cache-ttl-provider';
 import { Injector } from '../../common/injector';
 import { Injector } from '../../common/injector';
@@ -20,6 +21,7 @@ import { CacheTag } from './cache-tag.entity';
 export class SqlCacheStrategy implements CacheStrategy {
 export class SqlCacheStrategy implements CacheStrategy {
     protected cacheSize = 10_000;
     protected cacheSize = 10_000;
     protected ttlProvider: CacheTtlProvider;
     protected ttlProvider: CacheTtlProvider;
+    protected traceService: TraceService;
 
 
     constructor(config?: { cacheSize?: number; cacheTtlProvider?: CacheTtlProvider }) {
     constructor(config?: { cacheSize?: number; cacheTtlProvider?: CacheTtlProvider }) {
         if (config?.cacheSize) {
         if (config?.cacheSize) {
@@ -34,9 +36,14 @@ export class SqlCacheStrategy implements CacheStrategy {
     init(injector: Injector) {
     init(injector: Injector) {
         this.connection = injector.get(TransactionalConnection);
         this.connection = injector.get(TransactionalConnection);
         this.configService = injector.get(ConfigService);
         this.configService = injector.get(ConfigService);
+        this.traceService = injector.get(TraceService);
     }
     }
 
 
+    @Span('vendure.sql-cache-strategy.get')
     async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
     async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
+        const span = this.traceService.getSpan();
+        span?.setAttribute('cache.key', key);
+
         const hit = await this.connection.rawConnection.getRepository(CacheItem).findOne({
         const hit = await this.connection.rawConnection.getRepository(CacheItem).findOne({
             where: {
             where: {
                 key,
                 key,
@@ -46,16 +53,24 @@ export class SqlCacheStrategy implements CacheStrategy {
         if (hit) {
         if (hit) {
             if (!hit.expiresAt || (hit.expiresAt && this.ttlProvider.getTime() < hit.expiresAt.getTime())) {
             if (!hit.expiresAt || (hit.expiresAt && this.ttlProvider.getTime() < hit.expiresAt.getTime())) {
                 try {
                 try {
+                    span?.setAttribute('cache.hit', true);
+                    span?.setAttribute('cache.hit.expiresAt', hit.expiresAt?.toISOString() ?? 'never');
+                    span?.end();
                     return JSON.parse(hit.value);
                     return JSON.parse(hit.value);
                 } catch (e: any) {
                 } catch (e: any) {
                     /* */
                     /* */
                 }
                 }
             } else {
             } else {
+                span?.setAttribute('cache.hit', false);
+                span?.addEvent('cache.delete', {
+                    key,
+                });
                 await this.connection.rawConnection.getRepository(CacheItem).delete({
                 await this.connection.rawConnection.getRepository(CacheItem).delete({
                     key,
                     key,
                 });
                 });
             }
             }
         }
         }
+        span?.end();
     }
     }
 
 
     async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) {
     async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) {