Ver Fonte

feat: Introduce dedicated telemetry package

David Höck há 9 meses atrás
pai
commit
c104fdaf6b
39 ficheiros alterados com 1332 adições e 167 exclusões
  1. 1 1
      .eslintrc.js
  2. 17 0
      docker-compose.yml
  3. 808 38
      package-lock.json
  4. 1 0
      package.json
  5. 7 10
      packages/core/build/tsconfig.build.json
  6. 0 2
      packages/core/package.json
  7. 11 0
      packages/core/src/api/middleware/asset-interceptor-plugin.ts
  8. 1 1
      packages/core/src/api/resolvers/admin/product.resolver.ts
  9. 3 11
      packages/core/src/app.module.ts
  10. 9 10
      packages/core/src/cache/cache.service.ts
  11. 5 3
      packages/core/src/config/config.service.ts
  12. 7 0
      packages/core/src/event-bus/event-bus.ts
  13. 8 0
      packages/core/src/instrumentation.ts
  14. 5 5
      packages/core/src/job-queue/job-queue.service.ts
  15. 4 8
      packages/core/src/job-queue/job-queue.ts
  16. 14 4
      packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts
  17. 9 0
      packages/core/src/service/services/administrator.service.ts
  18. 27 1
      packages/core/src/service/services/asset.service.ts
  19. 8 2
      packages/core/src/service/services/auth.service.ts
  20. 17 1
      packages/core/src/service/services/channel.service.ts
  21. 33 7
      packages/core/src/service/services/collection.service.ts
  22. 9 2
      packages/core/src/service/services/country.service.ts
  23. 11 1
      packages/core/src/service/services/customer-group.service.ts
  24. 4 2
      packages/core/src/service/services/customer.service.ts
  25. 31 2
      packages/core/src/service/services/product-variant.service.ts
  26. 103 10
      packages/core/src/service/services/product.service.ts
  27. 0 1
      packages/dev-server/dev-config.ts
  28. 3 39
      packages/dev-server/instrumentation.ts
  29. 0 6
      packages/dev-server/package.json
  30. 37 0
      packages/telemetry/package.json
  31. 43 0
      packages/telemetry/src/decorator/span.ts
  32. 5 0
      packages/telemetry/src/index.ts
  33. 35 0
      packages/telemetry/src/instrumentation.ts
  34. 17 0
      packages/telemetry/src/tracing.plugin.ts
  35. 13 0
      packages/telemetry/src/tracing/trace.service.ts
  36. 1 0
      packages/telemetry/src/types.ts
  37. 5 0
      packages/telemetry/src/utils/span.ts
  38. 7 0
      packages/telemetry/tsconfig.build.json
  39. 13 0
      packages/telemetry/tsconfig.json

+ 1 - 1
.eslintrc.js

@@ -183,7 +183,7 @@ module.exports = {
         'id-denylist': 'off',
         'id-match': 'off',
         'import/order': [
-            'error',
+            'warn',
             {
                 alphabetize: {
                     caseInsensitive: true,

+ 17 - 0
docker-compose.yml

@@ -101,6 +101,23 @@ services:
             - ALLOW_EMPTY_PASSWORD=yes
         ports:
             - '6379:6379'
+
+    zipkin:
+        image: openzipkin/zipkin:latest
+        container_name: zipkin
+        # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables
+        environment:
+            - STORAGE_TYPE=mem
+        # Uncomment to enable self-tracing
+        # - SELF_TRACING_ENABLED=true
+        # Uncomment to increase heap size
+        # - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError
+        ports:
+            # Port used for the Zipkin UI and HTTP Api
+            - 9411:9411
+        # Uncomment to enable debug logging
+        # command: --logging.level.zipkin2=DEBUG
+
 volumes:
     postgres_16_data:
         driver: local

Diff do ficheiro suprimidas por serem muito extensas
+ 808 - 38
package-lock.json


+ 1 - 0
package.json

@@ -55,6 +55,7 @@
     "lerna": "^8.1.2",
     "lint-staged": "^10.5.4",
     "prettier": "^3.2.5",
+    "prettier-plugin-organize-imports": "^4.1.0",
     "rollup": "^4.18.0",
     "tinybench": "^2.6.0",
     "ts-node": "^10.9.2",

+ 7 - 10
packages/core/build/tsconfig.build.json

@@ -1,12 +1,9 @@
 {
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "removeComments": false,
-    "outDir": "../dist",
-    "rootDirs": ["../src"]
-  },
-  "files": [
-    "../src/index.ts",
-    "../typings.d.ts"
-  ]
+    "extends": "../tsconfig.json",
+    "compilerOptions": {
+        "removeComments": false,
+        "outDir": "../dist",
+        "rootDirs": ["../src"]
+    },
+    "files": ["../src/index.ts", "../typings.d.ts"]
 }

+ 0 - 2
packages/core/package.json

@@ -49,7 +49,6 @@
         "@nestjs/platform-express": "~11.0.12",
         "@nestjs/terminus": "~11.0.0",
         "@nestjs/typeorm": "~11.0.0",
-        "@opentelemetry/sdk-node": "^0.200.0",
         "@vendure/common": "3.2.2",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
@@ -72,7 +71,6 @@
         "mime-types": "^2.1.35",
         "ms": "^2.1.3",
         "nanoid": "^3.3.8",
-        "nestjs-otel": "^6.2.0",
         "picocolors": "^1.1.1",
         "progress": "^2.0.3",
         "reflect-metadata": "^0.2.2",

+ 11 - 0
packages/core/src/api/middleware/asset-interceptor-plugin.ts

@@ -1,8 +1,10 @@
 import { ApolloServerPlugin, GraphQLRequestListener, GraphQLServerContext } from '@apollo/server';
+import { getActiveSpan } from '@vendure/telemetry';
 import { DocumentNode, GraphQLNamedType, isUnionType } from 'graphql';
 
 import { AssetStorageStrategy } from '../../config/asset-storage-strategy/asset-storage-strategy';
 import { ConfigService } from '../../config/config.service';
+import { Span } from '../../instrumentation';
 import { GraphqlValueTransformer } from '../common/graphql-value-transformer';
 
 /**
@@ -41,10 +43,14 @@ export class AssetInterceptorPlugin implements ApolloServerPlugin {
         };
     }
 
+    @Span('vendure.asset-interceptor-plugin.prefix-asset-urls')
     private prefixAssetUrls(request: any, document: DocumentNode, data?: Record<string, unknown> | null) {
+        const span = getActiveSpan();
         const typeTree = this.graphqlValueTransformer.getOutputTypeTree(document);
         const toAbsoluteUrl = this.toAbsoluteUrl;
         if (!toAbsoluteUrl || !data) {
+            span?.addEvent('no-data');
+            span?.end();
             return;
         }
         this.graphqlValueTransformer.transformValues(typeTree, data, (value, type) => {
@@ -56,13 +62,18 @@ export class AssetInterceptorPlugin implements ApolloServerPlugin {
             if (isAssetType || isUnionWithAssetType) {
                 if (value && !Array.isArray(value)) {
                     if (value.preview) {
+                        span?.setAttribute('preview_relative_url', value.preview);
                         value.preview = toAbsoluteUrl(request, value.preview);
+                        span?.setAttribute('preview_absolute_url', value.preview);
                     }
                     if (value.source) {
+                        span?.setAttribute('source_relative_url', value.source);
                         value.source = toAbsoluteUrl(request, value.source);
+                        span?.setAttribute('source_absolute_url', value.source);
                     }
                 }
             }
+            span?.end();
             return value;
         });
     }

+ 1 - 1
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -29,8 +29,8 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { ErrorResultUnion } from '../../../common/error/error-result';
 import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
-import { Product } from '../../../entity/product/product.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
+import { Product } from '../../../entity/product/product.entity';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductService } from '../../../service/services/product.service';

+ 3 - 11
packages/core/src/app.module.ts

@@ -1,6 +1,6 @@
 import { MiddlewareConsumer, Module, NestModule, OnApplicationShutdown } from '@nestjs/common';
+import { getActiveSpan } from '@vendure/telemetry';
 import cookieSession from 'cookie-session';
-import { OpenTelemetryModule, Span, TraceService } from 'nestjs-otel';
 
 import { ApiModule } from './api/api.module';
 import { Middleware, MiddlewareHandler } from './common';
@@ -11,6 +11,7 @@ import { ConnectionModule } from './connection/connection.module';
 import { HealthCheckModule } from './health-check/health-check.module';
 import { I18nModule } from './i18n/i18n.module';
 import { I18nService } from './i18n/i18n.service';
+import { Span } from './instrumentation';
 import { PluginModule } from './plugin/plugin.module';
 import { ProcessContextModule } from './process-context/process-context.module';
 import { ServiceModule } from './service/service.module';
@@ -25,26 +26,17 @@ import { ServiceModule } from './service/service.module';
         HealthCheckModule,
         ServiceModule,
         ConnectionModule,
-        OpenTelemetryModule.forRoot({
-            metrics: {
-                hostMetrics: true, // Includes Host Metrics
-                apiMetrics: {
-                    enable: true, // Includes api metrics
-                },
-            },
-        }),
     ],
 })
 export class AppModule implements NestModule, OnApplicationShutdown {
     constructor(
         private configService: ConfigService,
         private i18nService: I18nService,
-        private traceService: TraceService,
     ) {}
 
     @Span('vendure.app-module.configure')
     configure(consumer: MiddlewareConsumer) {
-        const currentSpan = this.traceService.getSpan();
+        const currentSpan = getActiveSpan();
         const { adminApiPath, shopApiPath, middleware } = this.configService.apiOptions;
         const { cookieOptions } = this.configService.authOptions;
 

+ 9 - 10
packages/core/src/cache/cache.service.ts

@@ -1,10 +1,11 @@
 import { Injectable } from '@nestjs/common';
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
-import { Span, TraceService } from 'nestjs-otel';
+import { getActiveSpan } from '@vendure/telemetry';
 
 import { ConfigService } from '../config/config.service';
 import { Logger } from '../config/index';
 import { CacheStrategy, SetCacheKeyOptions } from '../config/system/cache-strategy';
+import { Span } from '../instrumentation';
 
 import { Cache, CacheConfig } from './cache';
 
@@ -21,10 +22,8 @@ import { Cache, CacheConfig } from './cache';
 @Injectable()
 export class CacheService {
     protected cacheStrategy: CacheStrategy;
-    constructor(
-        private configService: ConfigService,
-        private traceService: TraceService,
-    ) {
+
+    constructor(private configService: ConfigService) {
         this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
     }
 
@@ -37,7 +36,7 @@ export class CacheService {
      */
     @Span('vendure.cache.create-cache')
     createCache(config: CacheConfig): Cache {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.config', JSON.stringify(config));
         return new Cache(config, this);
     }
@@ -49,7 +48,7 @@ export class CacheService {
      */
     @Span('vendure.cache.get')
     async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.key', key);
         try {
             const result = await this.cacheStrategy.get(key);
@@ -78,7 +77,7 @@ export class CacheService {
         value: T,
         options?: SetCacheKeyOptions,
     ): Promise<void> {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.key', key);
         try {
             await this.cacheStrategy.set(key, value, options);
@@ -98,7 +97,7 @@ export class CacheService {
      */
     @Span('vendure.cache.delete')
     async delete(key: string): Promise<void> {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.key', key);
         try {
             await this.cacheStrategy.delete(key);
@@ -118,7 +117,7 @@ export class CacheService {
      */
     @Span('vendure.cache.invalidate-tags')
     async invalidateTags(tags: string[]): Promise<void> {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.tags', tags.join(', '));
         try {
             await this.cacheStrategy.invalidateTags(tags);

+ 5 - 3
packages/core/src/config/config.service.ts

@@ -1,8 +1,10 @@
 import { DynamicModule, Injectable, Type } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { Span, TraceService } from 'nestjs-otel';
+import { getActiveSpan } from '@vendure/telemetry';
 import { DataSourceOptions, getMetadataArgsStorage } from 'typeorm';
 
+import { Span } from '../instrumentation';
+
 import { getConfig } from './config-helpers';
 import { CustomFields } from './custom-field/custom-field-types';
 import { EntityIdStrategy } from './entity/entity-id-strategy';
@@ -30,7 +32,7 @@ export class ConfigService implements VendureConfig {
     private activeConfig: RuntimeVendureConfig;
     private allCustomFieldsConfig: Required<CustomFields> | undefined;
 
-    constructor(private readonly traceService: TraceService) {
+    constructor() {
         this.activeConfig = getConfig();
         if (this.activeConfig.authOptions.disableAuth) {
             // eslint-disable-next-line
@@ -125,7 +127,7 @@ export class ConfigService implements VendureConfig {
     private getCustomFieldsForAllEntities(): Required<CustomFields> {
         const definedCustomFields = this.activeConfig.customFields;
         const metadataArgsStorage = getMetadataArgsStorage();
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('customFields', JSON.stringify(definedCustomFields));
         // We need to check for any entities which have a "customFields" property but which are not
         // explicitly defined in the customFields config. This is because the customFields object

+ 7 - 0
packages/core/src/event-bus/event-bus.ts

@@ -1,6 +1,7 @@
 import { Injectable, OnModuleDestroy } from '@nestjs/common';
 import { Type } from '@vendure/common/lib/shared-types';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { getActiveSpan } from '@vendure/telemetry';
 import { Observable, Subject } from 'rxjs';
 import { filter, mergeMap, takeUntil } from 'rxjs/operators';
 import { EntityManager } from 'typeorm';
@@ -9,6 +10,7 @@ import { RequestContext } from '../api/common/request-context';
 import { TRANSACTION_MANAGER_KEY } from '../common/constants';
 import { Logger } from '../config/logger/vendure-logger';
 import { TransactionSubscriber, TransactionSubscriberError } from '../connection/transaction-subscriber';
+import { Span } from '../instrumentation';
 
 import { VendureEvent } from './vendure-event';
 
@@ -111,9 +113,14 @@ export class EventBus implements OnModuleDestroy {
      * await eventBus.publish(new SomeEvent());
      * ```
      */
+    @Span('vendure.event-bus publish')
     async publish<T extends VendureEvent>(event: T): Promise<void> {
+        const span = getActiveSpan();
+        span?.setAttribute('event', event.constructor.name);
+        span?.setAttribute('event-timestamp', event.createdAt.toISOString());
         this.eventStream.next(event);
         await this.executeBlockingEventHandlers(event);
+        span?.end();
     }
 
     /**

+ 8 - 0
packages/core/src/instrumentation.ts

@@ -0,0 +1,8 @@
+import { trace } from '@opentelemetry/api';
+import { createSpanDecorator } from '@vendure/telemetry';
+
+import pkg from '../package.json';
+
+export const tracer = trace.getTracer(pkg.name, pkg.version);
+
+export const Span = createSpanDecorator(tracer);

+ 5 - 5
packages/core/src/job-queue/job-queue.service.ts

@@ -1,8 +1,9 @@
 import { Injectable, OnModuleDestroy } from '@nestjs/common';
 import { JobQueue as GraphQlJobQueue } from '@vendure/common/lib/generated-types';
-import { Span, TraceService } from 'nestjs-otel';
+import { getActiveSpan } from '@vendure/telemetry';
 
 import { ConfigService, JobQueueStrategy, Logger } from '../config';
+import { Span } from '../instrumentation';
 
 import { loggerCtx } from './constants';
 import { Job } from './job';
@@ -58,7 +59,6 @@ export class JobQueueService implements OnModuleDestroy {
     constructor(
         private configService: ConfigService,
         private jobBufferService: JobBufferService,
-        private traceService: TraceService,
     ) {}
 
     /** @internal */
@@ -81,10 +81,10 @@ export class JobQueueService implements OnModuleDestroy {
         const wrappedProcessFn = this.createWrappedProcessFn(options.process);
         options = { ...options, process: wrappedProcessFn };
 
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('job-queue.name', options.name);
 
-        const queue = new JobQueue(options, this.jobQueueStrategy, this.jobBufferService, this.traceService);
+        const queue = new JobQueue(options, this.jobQueueStrategy, this.jobBufferService);
         if (this.hasStarted && this.shouldStartQueue(queue.name)) {
             await queue.start();
         }
@@ -102,7 +102,7 @@ export class JobQueueService implements OnModuleDestroy {
             if (!queue.started && this.shouldStartQueue(queue.name)) {
                 Logger.info(`Starting queue: ${queue.name}`, loggerCtx);
                 await queue.start();
-                const span = this.traceService.getSpan();
+                const span = getActiveSpan();
                 span?.setAttribute('job-queue.name', queue.name);
             }
         }

+ 4 - 8
packages/core/src/job-queue/job-queue.ts

@@ -1,15 +1,12 @@
-import { JobState } from '@vendure/common/lib/generated-types';
-import { Span, TraceService } from 'nestjs-otel';
-import { Subject, Subscription } from 'rxjs';
-import { throttleTime } from 'rxjs/operators';
+import { getActiveSpan } from '@vendure/telemetry';
 
 import { JobQueueStrategy } from '../config';
-import { Logger } from '../config/logger/vendure-logger';
+import { Span } from '../instrumentation';
 
 import { Job } from './job';
 import { JobBufferService } from './job-buffer/job-buffer.service';
 import { SubscribableJob } from './subscribable-job';
-import { CreateQueueOptions, JobConfig, JobData, JobOptions } from './types';
+import { CreateQueueOptions, JobData, JobOptions } from './types';
 
 /**
  * @description
@@ -38,7 +35,6 @@ export class JobQueue<Data extends JobData<Data> = object> {
         private options: CreateQueueOptions<Data>,
         private jobQueueStrategy: JobQueueStrategy,
         private jobBufferService: JobBufferService,
-        private traceService: TraceService,
     ) {}
 
     /** @internal */
@@ -99,7 +95,7 @@ export class JobQueue<Data extends JobData<Data> = object> {
             queueName: this.options.name,
             retries: options?.retries ?? 0,
         });
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('job.data', JSON.stringify(data));
         span?.setAttribute('job.retries', options?.retries ?? 0);
         span?.setAttribute('job.queueName', this.options.name);

+ 14 - 4
packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts

@@ -1,11 +1,12 @@
 import { JsonCompatible } from '@vendure/common/lib/shared-types';
-import { Span, TraceService } from 'nestjs-otel';
+import { getActiveSpan } from '@vendure/telemetry';
 
 import { CacheTtlProvider, DefaultCacheTtlProvider } from '../../cache/cache-ttl-provider';
 import { Injector } from '../../common/injector';
 import { ConfigService, Logger } from '../../config/index';
 import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy';
 import { TransactionalConnection } from '../../connection/index';
+import { Span } from '../../instrumentation';
 
 import { CacheItem } from './cache-item.entity';
 import { CacheTag } from './cache-tag.entity';
@@ -21,7 +22,6 @@ import { CacheTag } from './cache-tag.entity';
 export class SqlCacheStrategy implements CacheStrategy {
     protected cacheSize = 10_000;
     protected ttlProvider: CacheTtlProvider;
-    protected traceService: TraceService;
 
     constructor(config?: { cacheSize?: number; cacheTtlProvider?: CacheTtlProvider }) {
         if (config?.cacheSize) {
@@ -36,12 +36,11 @@ export class SqlCacheStrategy implements CacheStrategy {
     init(injector: Injector) {
         this.connection = injector.get(TransactionalConnection);
         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> {
-        const span = this.traceService.getSpan();
+        const span = getActiveSpan();
         span?.setAttribute('cache.key', key);
 
         const hit = await this.connection.rawConnection.getRepository(CacheItem).findOne({
@@ -73,7 +72,10 @@ export class SqlCacheStrategy implements CacheStrategy {
         span?.end();
     }
 
+    @Span('vendure.sql-cache-strategy.set')
     async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) {
+        const span = getActiveSpan();
+        span?.setAttribute('cache.key', key);
         const cacheSize = await this.connection.rawConnection.getRepository(CacheItem).count();
         if (cacheSize >= this.cacheSize) {
             // evict oldest
@@ -128,13 +130,21 @@ export class SqlCacheStrategy implements CacheStrategy {
         }
     }
 
+    @Span('vendure.sql-cache-strategy.delete')
     async delete(key: string) {
+        const span = getActiveSpan();
+        span?.setAttribute('cache.key', key);
+
         await this.connection.rawConnection.getRepository(CacheItem).delete({
             key,
         });
     }
 
+    @Span('vendure.sql-cache-strategy.invalidate-tags')
     async invalidateTags(tags: string[]) {
+        const span = getActiveSpan();
+        span?.setAttribute('cache.tags', tags.join(', '));
+
         await this.connection.withTransaction(async ctx => {
             const itemIds = await this.connection
                 .getRepository(ctx, CacheTag)

+ 9 - 0
packages/core/src/service/services/administrator.service.ts

@@ -21,6 +21,7 @@ import { User } from '../../entity/user/user.entity';
 import { EventBus } from '../../event-bus';
 import { AdministratorEvent } from '../../event-bus/events/administrator-event';
 import { RoleChangeEvent } from '../../event-bus/events/role-change-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PasswordCipher } from '../helpers/password-cipher/password-cipher';
@@ -60,6 +61,7 @@ export class AdministratorService {
      * @description
      * Get a paginated list of Administrators.
      */
+    @Span('AdministratorService.findAll')
     findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Administrator>,
@@ -82,6 +84,7 @@ export class AdministratorService {
      * @description
      * Get an Administrator by id.
      */
+    @Span('AdministratorService.findOne')
     findOne(
         ctx: RequestContext,
         administratorId: ID,
@@ -103,6 +106,7 @@ export class AdministratorService {
      * @description
      * Get an Administrator based on the User id.
      */
+    @Span('AdministratorService.findOneByUserId')
     findOneByUserId(
         ctx: RequestContext,
         userId: ID,
@@ -124,6 +128,7 @@ export class AdministratorService {
      * @description
      * Create a new Administrator.
      */
+    @Span('AdministratorService.create')
     async create(ctx: RequestContext, input: CreateAdministratorInput): Promise<Administrator> {
         await this.checkActiveUserCanGrantRoles(ctx, input.roleIds);
         const administrator = new Administrator(input);
@@ -149,6 +154,7 @@ export class AdministratorService {
      * @description
      * Update an existing Administrator.
      */
+    @Span('AdministratorService.update')
     async update(ctx: RequestContext, input: UpdateAdministratorInput): Promise<Administrator> {
         const administrator = await this.findOne(ctx, input.id);
         if (!administrator) {
@@ -251,6 +257,7 @@ export class AdministratorService {
      * @description
      * Soft deletes an Administrator (sets the `deletedAt` field).
      */
+    @Span('AdministratorService.softDelete')
     async softDelete(ctx: RequestContext, id: ID) {
         const administrator = await this.connection.getEntityOrThrow(ctx, Administrator, id, {
             relations: ['user'],
@@ -273,6 +280,7 @@ export class AdministratorService {
      * Resolves to `true` if the administrator ID belongs to the only Administrator
      * with SuperAdmin permissions.
      */
+    @Span('AdministratorService.isSoleSuperadmin')
     private async isSoleSuperadmin(ctx: RequestContext, id: ID) {
         const superAdminRole = await this.roleService.getSuperAdminRole(ctx);
         const allAdmins = await this.connection.getRepository(ctx, Administrator).find({
@@ -295,6 +303,7 @@ export class AdministratorService {
      *
      * @internal
      */
+    @Span('AdministratorService.ensureSuperAdminExists')
     private async ensureSuperAdminExists() {
         const { superadminCredentials } = this.configService.authOptions;
 

+ 27 - 1
packages/core/src/service/services/asset.service.ts

@@ -39,11 +39,12 @@ import { Asset } from '../../entity/asset/asset.entity';
 import { OrderableAsset } from '../../entity/asset/orderable-asset.entity';
 import { VendureEntity } from '../../entity/base/base.entity';
 import { Collection } from '../../entity/collection/collection.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { AssetChannelEvent } from '../../event-bus/events/asset-channel-event';
 import { AssetEvent } from '../../event-bus/events/asset-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -110,6 +111,7 @@ export class AssetService {
             });
     }
 
+    @Span('AssetService.findOne')
     findOne(ctx: RequestContext, id: ID, relations?: RelationPaths<Asset>): Promise<Asset | undefined> {
         return this.connection
             .findOneInChannel(ctx, Asset, id, ctx.channelId, {
@@ -118,6 +120,7 @@ export class AssetService {
             .then(result => result ?? undefined);
     }
 
+    @Span('AssetService.findAll')
     findAll(
         ctx: RequestContext,
         options?: AssetListOptions,
@@ -153,6 +156,7 @@ export class AssetService {
         }));
     }
 
+    @Span('AssetService.getFeaturedAsset')
     async getFeaturedAsset<T extends Omit<EntityWithAssets, 'assets'>>(
         ctx: RequestContext,
         entity: T,
@@ -192,6 +196,7 @@ export class AssetService {
      * Returns the Assets of an entity which has a well-ordered list of Assets, such as Product,
      * ProductVariant or Collection.
      */
+    @Span('AssetService.getEntityAssets')
     async getEntityAssets<T extends EntityWithAssets>(
         ctx: RequestContext,
         entity: T,
@@ -235,6 +240,7 @@ export class AssetService {
         return orderableAssets.sort((a, b) => a.position - b.position).map(a => a.asset);
     }
 
+    @Span('AssetService.updateFeaturedAsset')
     async updateFeaturedAsset<T extends EntityWithAssets>(
         ctx: RequestContext,
         entity: T,
@@ -289,6 +295,7 @@ export class AssetService {
      *
      * See the [Uploading Files docs](/guides/developer-guide/uploading-files) for an example of usage.
      */
+    @Span('AssetService.create')
     async create(ctx: RequestContext, input: CreateAssetInput): Promise<CreateAssetResult> {
         return new Promise(async (resolve, reject) => {
             const { createReadStream, filename, mimetype } = await input.file;
@@ -322,6 +329,7 @@ export class AssetService {
      * @description
      * Updates the name, focalPoint, tags & custom fields of an Asset.
      */
+    @Span('AssetService.update')
     async update(ctx: RequestContext, input: UpdateAssetInput): Promise<Asset> {
         const asset = await this.connection.getEntityOrThrow(ctx, Asset, input.id);
         if (input.focalPoint) {
@@ -344,6 +352,7 @@ export class AssetService {
      * Deletes an Asset after performing checks to ensure that the Asset is not currently in use
      * by a Product, ProductVariant or Collection.
      */
+    @Span('AssetService.delete')
     async delete(
         ctx: RequestContext,
         ids: ID[],
@@ -409,6 +418,7 @@ export class AssetService {
         return this.deleteUnconditional(ctx, assets);
     }
 
+    @Span('AssetService.assignToChannel')
     async assignToChannel(ctx: RequestContext, input: AssignAssetsToChannelInput): Promise<Asset[]> {
         const hasPermission = await this.roleService.userHasPermissionOnChannel(
             ctx,
@@ -452,6 +462,7 @@ export class AssetService {
         filePath: string,
         ctx?: RequestContext,
     ): Promise<CreateAssetResult>;
+    @Span('AssetService.createFromFileStream')
     async createFromFileStream(
         stream: ReadStream | Readable,
         maybeFilePathOrCtx?: string | RequestContext,
@@ -477,6 +488,7 @@ export class AssetService {
         }
     }
 
+    @Span('AssetService.getMimeType')
     private getMimeType(stream: Readable, filename: string): string {
         if (stream instanceof IncomingMessage) {
             const contentType = stream.headers['content-type'];
@@ -492,6 +504,7 @@ export class AssetService {
      * Unconditionally delete given assets.
      * Does not remove assets from channels
      */
+    @Span('AssetService.deleteUnconditional')
     private async deleteUnconditional(ctx: RequestContext, assets: Asset[]): Promise<DeletionResponse> {
         for (const asset of assets) {
             // Create a new asset so that the id is still available
@@ -514,6 +527,7 @@ export class AssetService {
     /**
      * Check if current user has permissions to delete assets from all channels
      */
+    @Span('AssetService.hasDeletePermissionForChannels')
     private async hasDeletePermissionForChannels(ctx: RequestContext, channelIds: ID[]): Promise<boolean> {
         const permissions = await Promise.all(
             channelIds.map(async channelId => {
@@ -523,6 +537,7 @@ export class AssetService {
         return !permissions.includes(false);
     }
 
+    @Span('AssetService.createAssetInternal')
     private async createAssetInternal(
         ctx: RequestContext,
         stream: Stream,
@@ -571,6 +586,7 @@ export class AssetService {
         return this.connection.getRepository(ctx, Asset).save(asset);
     }
 
+    @Span('AssetService.getSourceFileName')
     private async getSourceFileName(ctx: RequestContext, fileName: string): Promise<string> {
         const { assetOptions } = this.configService;
         return this.generateUniqueName(fileName, (name, conflict) =>
@@ -578,6 +594,7 @@ export class AssetService {
         );
     }
 
+    @Span('AssetService.getPreviewFileName')
     private async getPreviewFileName(ctx: RequestContext, fileName: string): Promise<string> {
         const { assetOptions } = this.configService;
         return this.generateUniqueName(fileName, (name, conflict) =>
@@ -585,6 +602,7 @@ export class AssetService {
         );
     }
 
+    @Span('AssetService.generateUniqueName')
     private async generateUniqueName(
         inputFileName: string,
         generateNameFn: (fileName: string, conflictName?: string) => string,
@@ -597,6 +615,7 @@ export class AssetService {
         return outputFileName;
     }
 
+    @Span('AssetService.getDimensions')
     private getDimensions(imageFile: Buffer): { width: number; height: number } {
         try {
             const { width, height } = sizeOf(imageFile);
@@ -607,6 +626,7 @@ export class AssetService {
         }
     }
 
+    @Span('AssetService.createOrderableAssets')
     private createOrderableAssets(
         ctx: RequestContext,
         entity: EntityWithAssets,
@@ -616,6 +636,7 @@ export class AssetService {
         return this.connection.getRepository(ctx, orderableAssets[0].constructor).save(orderableAssets);
     }
 
+    @Span('AssetService.getOrderableAsset')
     private getOrderableAsset(
         ctx: RequestContext,
         entity: EntityWithAssets,
@@ -631,6 +652,7 @@ export class AssetService {
         });
     }
 
+    @Span('AssetService.removeExistingOrderableAssets')
     private async removeExistingOrderableAssets(ctx: RequestContext, entity: EntityWithAssets) {
         const propertyName = this.getHostEntityIdProperty(entity);
         const orderableAssetType = this.getOrderableAssetType(ctx, entity);
@@ -639,6 +661,7 @@ export class AssetService {
         });
     }
 
+    @Span('AssetService.getOrderableAssetType')
     private getOrderableAssetType(ctx: RequestContext, entity: EntityWithAssets): Type<OrderableAsset> {
         const assetRelation = this.connection
             .getRepository(ctx, entity.constructor)
@@ -649,6 +672,7 @@ export class AssetService {
         return assetRelation.type as Type<OrderableAsset>;
     }
 
+    @Span('AssetService.getHostEntityIdProperty')
     private getHostEntityIdProperty(entity: EntityWithAssets): string {
         const entityName = entity.constructor.name;
         switch (entityName) {
@@ -663,6 +687,7 @@ export class AssetService {
         }
     }
 
+    @Span('AssetService.validateMimeType')
     private validateMimeType(mimeType: string): boolean {
         const [type, subtype] = mimeType.split('/');
         const typeMatches = this.permittedMimeTypes.filter(t => t.type === type);
@@ -678,6 +703,7 @@ export class AssetService {
     /**
      * Find the entities which reference the given Asset as a featuredAsset.
      */
+    @Span('AssetService.findAssetUsages')
     private async findAssetUsages(
         ctx: RequestContext,
         asset: Asset,

+ 8 - 2
packages/core/src/service/services/auth.service.ts

@@ -6,14 +6,14 @@ import { RequestContext } from '../../api/common/request-context';
 import { InternalServerError } from '../../common/error/errors';
 import { InvalidCredentialsError } from '../../common/error/generated-graphql-admin-errors';
 import {
-    InvalidCredentialsError as ShopInvalidCredentialsError,
     NotVerifiedError,
+    InvalidCredentialsError as ShopInvalidCredentialsError,
 } from '../../common/error/generated-graphql-shop-errors';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import {
+    NATIVE_AUTH_STRATEGY_NAME,
     NativeAuthenticationData,
     NativeAuthenticationStrategy,
-    NATIVE_AUTH_STRATEGY_NAME,
 } from '../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -24,6 +24,7 @@ import { EventBus } from '../../event-bus/event-bus';
 import { AttemptedLoginEvent } from '../../event-bus/events/attempted-login-event';
 import { LoginEvent } from '../../event-bus/events/login-event';
 import { LogoutEvent } from '../../event-bus/events/logout-event';
+import { Span } from '../../instrumentation';
 
 import { SessionService } from './session.service';
 
@@ -46,6 +47,7 @@ export class AuthService {
      * @description
      * Authenticates a user's credentials and if okay, creates a new {@link AuthenticatedSession}.
      */
+    @Span('AuthService.authenticate')
     async authenticate(
         ctx: RequestContext,
         apiType: ApiType,
@@ -72,6 +74,7 @@ export class AuthService {
         return this.createAuthenticatedSessionForUser(ctx, authenticateResult, authenticationStrategy.name);
     }
 
+    @Span('AuthService.createAuthenticatedSessionForUser')
     async createAuthenticatedSessionForUser(
         ctx: RequestContext,
         user: User,
@@ -113,6 +116,7 @@ export class AuthService {
      * Verify the provided password against the one we have for the given user. Requires
      * the {@link NativeAuthenticationStrategy} to be configured.
      */
+    @Span('AuthService.verifyUserPassword')
     async verifyUserPassword(
         ctx: RequestContext,
         userId: ID,
@@ -133,6 +137,7 @@ export class AuthService {
      * @description
      * Deletes all sessions for the user associated with the given session token.
      */
+    @Span('AuthService.destroyAuthenticatedSession')
     async destroyAuthenticatedSession(ctx: RequestContext, sessionToken: string): Promise<void> {
         const session = await this.connection.getRepository(ctx, AuthenticatedSession).findOne({
             where: { token: sessionToken },
@@ -157,6 +162,7 @@ export class AuthService {
         method: typeof NATIVE_AUTH_STRATEGY_NAME,
     ): NativeAuthenticationStrategy;
     private getAuthenticationStrategy(apiType: ApiType, method: string): AuthenticationStrategy;
+    @Span('AuthService.getAuthenticationStrategy')
     private getAuthenticationStrategy(apiType: ApiType, method: string): AuthenticationStrategy {
         const { authOptions } = this.configService;
         const strategies =

+ 17 - 1
packages/core/src/service/services/channel.service.ts

@@ -39,12 +39,12 @@ import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus';
 import { ChangeChannelEvent } from '../../event-bus/events/change-channel-event';
 import { ChannelEvent } from '../../event-bus/events/channel-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { GlobalSettingsService } from './global-settings.service';
-
 /**
  * @description
  * Contains methods relating to {@link Channel} entities.
@@ -80,6 +80,7 @@ export class ChannelService {
      *
      * @internal
      */
+    @Span('ChannelService.createCache')
     async createCache(): Promise<SelfRefreshingCache<Channel[], [RequestContext]>> {
         return createSelfRefreshingCache({
             name: 'ChannelService.allChannels',
@@ -114,6 +115,7 @@ export class ChannelService {
      * specified in the RequestContext. This method will not save the entity to the database, but
      * assigns the `channels` property of the entity.
      */
+    @Span('ChannelService.assignToCurrentChannel')
     async assignToCurrentChannel<T extends ChannelAware & VendureEntity>(
         entity: T,
         ctx: RequestContext,
@@ -136,6 +138,7 @@ export class ChannelService {
      * @returns A promise that resolves to an array of objects, each containing a channel ID.
      * @private
      */
+    @Span('ChannelService.getAssignedEntityChannels')
     private async getAssignedEntityChannels<T extends ChannelAware & VendureEntity>(
         ctx: RequestContext,
         entityType: Type<T>,
@@ -174,6 +177,7 @@ export class ChannelService {
      * @description
      * Assigns the entity to the given Channels and saves all changes to the database.
      */
+    @Span('ChannelService.assignToChannels')
     async assignToChannels<T extends ChannelAware & VendureEntity>(
         ctx: RequestContext,
         entityType: Type<T>,
@@ -217,6 +221,7 @@ export class ChannelService {
      * @description
      * Removes the entity from the given Channels and saves.
      */
+    @Span('ChannelService.removeFromChannels')
     async removeFromChannels<T extends ChannelAware & VendureEntity>(
         ctx: RequestContext,
         entityType: Type<T>,
@@ -259,6 +264,7 @@ export class ChannelService {
      */
     async getChannelFromToken(token: string): Promise<Channel>;
     async getChannelFromToken(ctx: RequestContext, token: string): Promise<Channel>;
+    @Span('ChannelService.getChannelFromToken')
     async getChannelFromToken(ctxOrToken: RequestContext | string, token?: string): Promise<Channel> {
         const [ctx, channelToken] =
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -281,6 +287,7 @@ export class ChannelService {
      * @description
      * Returns the default Channel.
      */
+    @Span('ChannelService.getDefaultChannel')
     async getDefaultChannel(ctx?: RequestContext): Promise<Channel> {
         const allChannels = await this.allChannels.value(ctx);
         const defaultChannel = allChannels.find(channel => channel.code === DEFAULT_CHANNEL_CODE);
@@ -291,6 +298,7 @@ export class ChannelService {
         return defaultChannel;
     }
 
+    @Span('ChannelService.findAll')
     findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Channel>,
@@ -308,6 +316,7 @@ export class ChannelService {
             }));
     }
 
+    @Span('ChannelService.findOne')
     findOne(ctx: RequestContext, id: ID): Promise<Channel | undefined> {
         return this.connection
             .getRepository(ctx, Channel)
@@ -315,6 +324,7 @@ export class ChannelService {
             .then(result => result ?? undefined);
     }
 
+    @Span('ChannelService.create')
     async create(
         ctx: RequestContext,
         input: CreateChannelInput,
@@ -360,6 +370,7 @@ export class ChannelService {
         return newChannel;
     }
 
+    @Span('ChannelService.update')
     async update(
         ctx: RequestContext,
         input: UpdateChannelInput,
@@ -456,6 +467,7 @@ export class ChannelService {
         return assertFound(this.findOne(ctx, channel.id));
     }
 
+    @Span('ChannelService.delete')
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
         const channel = await this.connection.getEntityOrThrow(ctx, Channel, id);
         if (channel.code === DEFAULT_CHANNEL_CODE)
@@ -482,6 +494,7 @@ export class ChannelService {
      * Type guard method which returns true if the given entity is an
      * instance of a class which implements the {@link ChannelAware} interface.
      */
+    @Span('ChannelService.isChannelAware')
     public isChannelAware(entity: VendureEntity): entity is VendureEntity & ChannelAware {
         const entityType = Object.getPrototypeOf(entity).constructor;
         return !!this.connection.rawConnection
@@ -492,6 +505,7 @@ export class ChannelService {
     /**
      * Ensures channel cache exists. If not, this method creates one.
      */
+    @Span('ChannelService.ensureCacheExists')
     private async ensureCacheExists() {
         if (this.allChannels) {
             return;
@@ -504,6 +518,7 @@ export class ChannelService {
      * There must always be a default Channel. If none yet exists, this method creates one.
      * Also ensures the default Channel token matches the defaultChannelToken config setting.
      */
+    @Span('ChannelService.ensureDefaultChannelExists')
     private async ensureDefaultChannelExists() {
         const { defaultChannelToken } = this.configService;
         let defaultChannel = await this.connection.rawConnection.getRepository(Channel).findOne({
@@ -541,6 +556,7 @@ export class ChannelService {
         }
     }
 
+    @Span('ChannelService.validateDefaultLanguageCode')
     private async validateDefaultLanguageCode(
         ctx: RequestContext,
         input: CreateChannelInput | UpdateChannelInput,

+ 33 - 7
packages/core/src/service/services/collection.service.ts

@@ -39,6 +39,7 @@ import { CollectionEvent } from '../../event-bus/events/collection-event';
 import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
+import { Span } from '../../instrumentation';
 import { JobQueue } from '../../job-queue/job-queue';
 import { JobQueueService } from '../../job-queue/job-queue.service';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
@@ -177,7 +178,7 @@ export class CollectionService implements OnModuleInit {
                             // To avoid performance issues on huge collections we first split the affected variant ids into chunks
                             this.chunkArray(affectedVariantIds, 50000).map(chunk =>
                                 this.eventBus.publish(
-                                    new CollectionModificationEvent(ctx, collection as Collection, chunk),
+                                    new CollectionModificationEvent(ctx, collection, chunk),
                                 ),
                             );
                         }
@@ -188,6 +189,7 @@ export class CollectionService implements OnModuleInit {
         });
     }
 
+    @Span('CollectionService.findAll')
     async findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Collection> & { topLevelOnly?: boolean },
@@ -218,6 +220,7 @@ export class CollectionService implements OnModuleInit {
         });
     }
 
+    @Span('CollectionService.findOne')
     async findOne(
         ctx: RequestContext,
         collectionId: ID,
@@ -239,6 +242,7 @@ export class CollectionService implements OnModuleInit {
         return this.translator.translate(collection, ctx, ['parent']);
     }
 
+    @Span('CollectionService.findByIds')
     async findByIds(
         ctx: RequestContext,
         ids: ID[],
@@ -253,6 +257,7 @@ export class CollectionService implements OnModuleInit {
         );
     }
 
+    @Span('CollectionService.findOneBySlug')
     async findOneBySlug(
         ctx: RequestContext,
         slug: string,
@@ -284,10 +289,12 @@ export class CollectionService implements OnModuleInit {
      * @description
      * Returns all configured CollectionFilters, as specified by the {@link CatalogOptions}.
      */
+    @Span('CollectionService.getAvailableFilters')
     getAvailableFilters(ctx: RequestContext): ConfigurableOperationDefinition[] {
         return this.configService.catalogOptions.collectionFilters.map(f => f.toGraphQlType(ctx));
     }
 
+    @Span('CollectionService.getParent')
     async getParent(ctx: RequestContext, collectionId: ID): Promise<Collection | undefined> {
         const parent = await this.connection
             .getRepository(ctx, Collection)
@@ -311,6 +318,7 @@ export class CollectionService implements OnModuleInit {
      * @description
      * Returns all child Collections of the Collection with the given id.
      */
+    @Span('CollectionService.getChildren')
     async getChildren(ctx: RequestContext, collectionId: ID): Promise<Collection[]> {
         return this.getDescendants(ctx, collectionId, 1);
     }
@@ -320,6 +328,7 @@ export class CollectionService implements OnModuleInit {
      * Returns an array of name/id pairs representing all ancestor Collections up
      * to the Root Collection.
      */
+    @Span('CollectionService.getBreadcrumbs')
     async getBreadcrumbs(
         ctx: RequestContext,
         collection: Collection,
@@ -343,6 +352,7 @@ export class CollectionService implements OnModuleInit {
      * @description
      * Returns all Collections which are associated with the given Product ID.
      */
+    @Span('CollectionService.getCollectionsByProductId')
     async getCollectionsByProductId(
         ctx: RequestContext,
         productId: ID,
@@ -370,6 +380,7 @@ export class CollectionService implements OnModuleInit {
      * Returns the descendants of a Collection as a flat array. The depth of the traversal can be limited
      * with the maxDepth argument. So to get only the immediate children, set maxDepth = 1.
      */
+    @Span('CollectionService.getDescendants')
     async getDescendants(
         ctx: RequestContext,
         rootId: ID,
@@ -435,6 +446,7 @@ export class CollectionService implements OnModuleInit {
             });
     }
 
+    @Span('CollectionService.previewCollectionVariants')
     async previewCollectionVariants(
         ctx: RequestContext,
         input: PreviewCollectionVariantsInput,
@@ -464,8 +476,8 @@ export class CollectionService implements OnModuleInit {
         for (const filterType of collectionFilters) {
             const filtersOfType = applicableFilters.filter(f => f.code === filterType.code);
             if (filtersOfType.length) {
-                for (const filter of filtersOfType) {
-                    qb = filterType.apply(qb, filter.args);
+                for (const f of filtersOfType) {
+                    qb = filterType.apply(qb, f.args);
                 }
             }
         }
@@ -475,6 +487,7 @@ export class CollectionService implements OnModuleInit {
         }));
     }
 
+    @Span('CollectionService.create')
     async create(ctx: RequestContext, input: CreateCollectionInput): Promise<Translated<Collection>> {
         await this.slugValidator.validateSlugs(ctx, input, CollectionTranslation);
         const collection = await this.translatableSaver.create({
@@ -507,6 +520,7 @@ export class CollectionService implements OnModuleInit {
         return assertFound(this.findOne(ctx, collection.id));
     }
 
+    @Span('CollectionService.update')
     async update(ctx: RequestContext, input: UpdateCollectionInput): Promise<Translated<Collection>> {
         await this.slugValidator.validateSlugs(ctx, input, CollectionTranslation);
         const collection = await this.translatableSaver.update({
@@ -536,6 +550,7 @@ export class CollectionService implements OnModuleInit {
         return assertFound(this.findOne(ctx, collection.id));
     }
 
+    @Span('CollectionService.delete')
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
         const collection = await this.connection.getEntityOrThrow(ctx, Collection, id, {
             channelId: ctx.channelId,
@@ -571,6 +586,7 @@ export class CollectionService implements OnModuleInit {
      * Moves a Collection by specifying the parent Collection ID, and an index representing the order amongst
      * its siblings.
      */
+    @Span('CollectionService.move')
     async move(ctx: RequestContext, input: MoveCollectionInput): Promise<Translated<Collection>> {
         const target = await this.connection.getEntityOrThrow(ctx, Collection, input.collectionId, {
             channelId: ctx.channelId,
@@ -619,6 +635,7 @@ export class CollectionService implements OnModuleInit {
      *
      * @since 3.1.3
      */
+    @Span('CollectionService.setApplyAllFiltersOnProductUpdates')
     setApplyAllFiltersOnProductUpdates(applyAllFiltersOnProductUpdates: boolean) {
         this.applyAllFiltersOnProductUpdates = applyAllFiltersOnProductUpdates;
     }
@@ -632,6 +649,7 @@ export class CollectionService implements OnModuleInit {
      *
      * @since 3.1.3
      */
+    @Span('CollectionService.triggerApplyFiltersJob')
     async triggerApplyFiltersJob(
         ctx: RequestContext,
         options?: { collectionIds?: ID[]; applyToChangedVariantsOnly?: boolean },
@@ -649,13 +667,14 @@ export class CollectionService implements OnModuleInit {
         );
     }
 
+    @Span('CollectionService.getCollectionFiltersFromInput')
     private getCollectionFiltersFromInput(
         input: CreateCollectionInput | UpdateCollectionInput | PreviewCollectionVariantsInput,
     ): ConfigurableOperation[] {
         const filters: ConfigurableOperation[] = [];
         if (input.filters) {
-            for (const filter of input.filters) {
-                filters.push(this.configArgService.parseInput('CollectionFilter', filter));
+            for (const f of input.filters) {
+                filters.push(this.configArgService.parseInput('CollectionFilter', f));
             }
         }
         return filters;
@@ -673,6 +692,7 @@ export class CollectionService implements OnModuleInit {
     /**
      * Applies the CollectionFilters and returns the IDs of ProductVariants that need to be added or removed.
      */
+    @Span('CollectionService.applyCollectionFiltersInternal')
     private async applyCollectionFiltersInternal(
         collection: Collection,
         applyToChangedVariantsOnly = true,
@@ -699,8 +719,8 @@ export class CollectionService implements OnModuleInit {
         for (const filterType of collectionFilters) {
             const filtersOfType = filters.filter(f => f.code === filterType.code);
             if (filtersOfType.length) {
-                for (const filter of filtersOfType) {
-                    filteredQb = filterType.apply(filteredQb, filter.args);
+                for (const f of filtersOfType) {
+                    filteredQb = filterType.apply(filteredQb, f.args);
                 }
             }
         }
@@ -788,6 +808,7 @@ export class CollectionService implements OnModuleInit {
      * Gets all filters of ancestor Collections while respecting the `inheritFilters` setting of each.
      * As soon as `inheritFilters === false` is encountered, the collected filters are returned.
      */
+    @Span('CollectionService.getAncestorFilters')
     private async getAncestorFilters(collection: Collection): Promise<ConfigurableOperation[]> {
         const ancestorFilters: ConfigurableOperation[] = [];
         if (collection.inheritFilters) {
@@ -805,6 +826,7 @@ export class CollectionService implements OnModuleInit {
     /**
      * Returns the IDs of the Collection's ProductVariants.
      */
+    @Span('CollectionService.getCollectionProductVariantIds')
     async getCollectionProductVariantIds(collection: Collection, ctx?: RequestContext): Promise<ID[]> {
         if (collection.productVariants) {
             return collection.productVariants.map(v => v.id);
@@ -823,6 +845,7 @@ export class CollectionService implements OnModuleInit {
     /**
      * Returns the next position value in the given parent collection.
      */
+    @Span('CollectionService.getNextPositionInParent')
     private async getNextPositionInParent(ctx: RequestContext, maybeParentId?: ID): Promise<number> {
         const parentId = maybeParentId || (await this.getRootCollection(ctx)).id;
         const result = await this.connection
@@ -854,6 +877,7 @@ export class CollectionService implements OnModuleInit {
         }
     }
 
+    @Span('CollectionService.getRootCollection')
     private async getRootCollection(ctx: RequestContext): Promise<Collection> {
         const cachedRoot = this.rootCollection;
 
@@ -904,6 +928,7 @@ export class CollectionService implements OnModuleInit {
      * @description
      * Assigns Collections to the specified Channel
      */
+    @Span('CollectionService.assignCollectionsToChannel')
     async assignCollectionsToChannel(
         ctx: RequestContext,
         input: AssignCollectionsToChannelInput,
@@ -948,6 +973,7 @@ export class CollectionService implements OnModuleInit {
      * @description
      * Remove Collections from the specified Channel
      */
+    @Span('CollectionService.removeCollectionsFromChannel')
     async removeCollectionsFromChannel(
         ctx: RequestContext,
         input: RemoveCollectionsFromChannelInput,

+ 9 - 2
packages/core/src/service/services/country.service.ts

@@ -5,7 +5,7 @@ import {
     DeletionResult,
     UpdateCountryInput,
 } from '@vendure/common/lib/generated-types';
-import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
+import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
@@ -17,9 +17,9 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Address } from '../../entity';
 import { Country } from '../../entity/region/country.entity';
 import { RegionTranslation } from '../../entity/region/region-translation.entity';
-import { Region } from '../../entity/region/region.entity';
 import { EventBus } from '../../event-bus';
 import { CountryEvent } from '../../event-bus/events/country-event';
+import { Span } from '../../instrumentation';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatorService } from '../helpers/translator/translator.service';
@@ -40,6 +40,7 @@ export class CountryService {
         private translator: TranslatorService,
     ) {}
 
+    @Span('CountryService.findAll')
     findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Country>,
@@ -57,6 +58,7 @@ export class CountryService {
             });
     }
 
+    @Span('CountryService.findOne')
     findOne(
         ctx: RequestContext,
         countryId: ID,
@@ -72,6 +74,7 @@ export class CountryService {
      * @description
      * Returns an array of enabled Countries, intended for use in a public-facing (ie. Shop) API.
      */
+    @Span('CountryService.findAllAvailable')
     findAllAvailable(ctx: RequestContext): Promise<Array<Translated<Country>>> {
         return this.connection
             .getRepository(ctx, Country)
@@ -83,6 +86,7 @@ export class CountryService {
      * @description
      * Returns a Country based on its ISO country code.
      */
+    @Span('CountryService.findOneByCode')
     async findOneByCode(ctx: RequestContext, countryCode: string): Promise<Translated<Country>> {
         const country = await this.connection.getRepository(ctx, Country).findOne({
             where: {
@@ -95,6 +99,7 @@ export class CountryService {
         return this.translator.translate(country, ctx);
     }
 
+    @Span('CountryService.create')
     async create(ctx: RequestContext, input: CreateCountryInput): Promise<Translated<Country>> {
         const country = await this.translatableSaver.create({
             ctx,
@@ -106,6 +111,7 @@ export class CountryService {
         return assertFound(this.findOne(ctx, country.id));
     }
 
+    @Span('CountryService.update')
     async update(ctx: RequestContext, input: UpdateCountryInput): Promise<Translated<Country>> {
         const country = await this.translatableSaver.update({
             ctx,
@@ -117,6 +123,7 @@ export class CountryService {
         return assertFound(this.findOne(ctx, country.id));
     }
 
+    @Span('CountryService.delete')
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
         const country = await this.connection.getEntityOrThrow(ctx, Country, id);
         const addressesUsingCountry = await this.connection

+ 11 - 1
packages/core/src/service/services/customer-group.service.ts

@@ -17,11 +17,12 @@ import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { UserInputError } from '../../common/error/errors';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
+import { Customer } from '../../entity/customer/customer.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { CustomerGroupChangeEvent } from '../../event-bus/events/customer-group-change-event';
 import { CustomerGroupEvent } from '../../event-bus/events/customer-group-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -44,6 +45,7 @@ export class CustomerGroupService {
         private customFieldRelationService: CustomFieldRelationService,
     ) {}
 
+    @Span('CustomerGroupService.findAll')
     findAll(
         ctx: RequestContext,
         options?: CustomerGroupListOptions,
@@ -55,6 +57,7 @@ export class CustomerGroupService {
             .then(([items, totalItems]) => ({ items, totalItems }));
     }
 
+    @Span('CustomerGroupService.findOne')
     findOne(
         ctx: RequestContext,
         customerGroupId: ID,
@@ -70,6 +73,7 @@ export class CustomerGroupService {
      * @description
      * Returns a {@link PaginatedList} of all the Customers in the group.
      */
+    @Span('CustomerGroupService.getGroupCustomers')
     getGroupCustomers(
         ctx: RequestContext,
         customerGroupId: ID,
@@ -86,6 +90,7 @@ export class CustomerGroupService {
             .then(([items, totalItems]) => ({ items, totalItems }));
     }
 
+    @Span('CustomerGroupService.create')
     async create(ctx: RequestContext, input: CreateCustomerGroupInput): Promise<CustomerGroup> {
         const customerGroup = new CustomerGroup(input);
 
@@ -111,6 +116,7 @@ export class CustomerGroupService {
         return assertFound(this.findOne(ctx, savedCustomerGroup.id));
     }
 
+    @Span('CustomerGroupService.update')
     async update(ctx: RequestContext, input: UpdateCustomerGroupInput): Promise<CustomerGroup> {
         const customerGroup = await this.connection.getEntityOrThrow(ctx, CustomerGroup, input.id);
         const updatedCustomerGroup = patchEntity(customerGroup, input);
@@ -125,6 +131,7 @@ export class CustomerGroupService {
         return assertFound(this.findOne(ctx, customerGroup.id));
     }
 
+    @Span('CustomerGroupService.delete')
     async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
         const group = await this.connection.getEntityOrThrow(ctx, CustomerGroup, id);
         try {
@@ -142,6 +149,7 @@ export class CustomerGroupService {
         }
     }
 
+    @Span('CustomerGroupService.addCustomersToGroup')
     async addCustomersToGroup(
         ctx: RequestContext,
         input: MutationAddCustomersToGroupArgs,
@@ -168,6 +176,7 @@ export class CustomerGroupService {
         return assertFound(this.findOne(ctx, group.id));
     }
 
+    @Span('CustomerGroupService.removeCustomersFromGroup')
     async removeCustomersFromGroup(
         ctx: RequestContext,
         input: MutationRemoveCustomersFromGroupArgs,
@@ -193,6 +202,7 @@ export class CustomerGroupService {
         return assertFound(this.findOne(ctx, group.id));
     }
 
+    @Span('CustomerGroupService.getCustomersFromIds')
     private getCustomersFromIds(ctx: RequestContext, ids: ID[]): Promise<Customer[]> | Customer[] {
         if (ids.length === 0) {
             return new Array<Customer>();

+ 4 - 2
packages/core/src/service/services/customer.service.ts

@@ -11,7 +11,6 @@ import {
     CreateCustomerInput,
     CreateCustomerResult,
     CustomerFilterParameter,
-    CustomerListOptions,
     DeletionResponse,
     DeletionResult,
     HistoryEntryType,
@@ -46,8 +45,8 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { Address } from '../../entity/address/address.entity';
 import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
 import { Channel } from '../../entity/channel/channel.entity';
-import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
+import { Customer } from '../../entity/customer/customer.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
 import { Order } from '../../entity/order/order.entity';
 import { User } from '../../entity/user/user.entity';
@@ -60,6 +59,7 @@ import { IdentifierChangeEvent } from '../../event-bus/events/identifier-change-
 import { IdentifierChangeRequestEvent } from '../../event-bus/events/identifier-change-request-event';
 import { PasswordResetEvent } from '../../event-bus/events/password-reset-event';
 import { PasswordResetVerifiedEvent } from '../../event-bus/events/password-reset-verified-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatorService } from '../helpers/translator/translator.service';
@@ -92,6 +92,7 @@ export class CustomerService {
         private translator: TranslatorService,
     ) {}
 
+    @Span('CustomerService.findAll')
     findAll(
         ctx: RequestContext,
         options: ListQueryOptions<Customer> | undefined,
@@ -118,6 +119,7 @@ export class CustomerService {
             .then(([items, totalItems]) => ({ items, totalItems }));
     }
 
+    @Span('CustomerService.findOne')
     findOne(
         ctx: RequestContext,
         id: ID,

+ 31 - 2
packages/core/src/service/services/product-variant.service.ts

@@ -35,14 +35,15 @@ import {
     TaxCategory,
 } from '../../entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { ProductVariantChannelEvent } from '../../event-bus/events/product-variant-channel-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
 import { ProductVariantPriceEvent } from '../../event-bus/events/product-variant-price-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
@@ -88,6 +89,7 @@ export class ProductVariantService {
         private translator: TranslatorService,
     ) {}
 
+    @Span('ProductVariantService.findAll')
     async findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<ProductVariant>,
@@ -121,6 +123,7 @@ export class ProductVariantService {
             });
     }
 
+    @Span('ProductVariantService.findOne')
     findOne(
         ctx: RequestContext,
         productVariantId: ID,
@@ -143,6 +146,7 @@ export class ProductVariantService {
             });
     }
 
+    @Span('ProductVariantService.findByIds')
     findByIds(ctx: RequestContext, ids: ID[]): Promise<Array<Translated<ProductVariant>>> {
         return this.connection
             .findByIdsInChannel(ctx, ProductVariant, ids, ctx.channelId, {
@@ -158,6 +162,7 @@ export class ProductVariantService {
             .then(variants => this.applyPricesAndTranslateVariants(ctx, variants));
     }
 
+    @Span('ProductVariantService.getVariantsByProductId')
     getVariantsByProductId(
         ctx: RequestContext,
         productId: ID,
@@ -204,6 +209,7 @@ export class ProductVariantService {
      * @description
      * Returns a {@link PaginatedList} of all ProductVariants associated with the given Collection.
      */
+    @Span('ProductVariantService.getVariantsByCollectionId')
     getVariantsByCollectionId(
         ctx: RequestContext,
         collectionId: ID,
@@ -239,6 +245,7 @@ export class ProductVariantService {
      * @description
      * Returns all Channels to which the ProductVariant is assigned.
      */
+    @Span('ProductVariantService.getProductVariantChannels')
     async getProductVariantChannels(ctx: RequestContext, productVariantId: ID): Promise<Channel[]> {
         const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId, {
             relations: ['channels'],
@@ -247,6 +254,7 @@ export class ProductVariantService {
         return variant.channels;
     }
 
+    @Span('ProductVariantService.getProductVariantPrices')
     async getProductVariantPrices(ctx: RequestContext, productVariantId: ID): Promise<ProductVariantPrice[]> {
         return this.connection
             .getRepository(ctx, ProductVariantPrice)
@@ -260,6 +268,7 @@ export class ProductVariantService {
      * @description
      * Returns the ProductVariant associated with the given {@link OrderLine}.
      */
+    @Span('ProductVariantService.getVariantByOrderLineId')
     async getVariantByOrderLineId(ctx: RequestContext, orderLineId: ID): Promise<Translated<ProductVariant>> {
         const { productVariant } = await this.connection.getEntityOrThrow(ctx, OrderLine, orderLineId, {
             relations: ['productVariant', 'productVariant.taxCategory'],
@@ -272,6 +281,7 @@ export class ProductVariantService {
      * @description
      * Returns the {@link ProductOption}s for the given ProductVariant.
      */
+    @Span('ProductVariantService.getOptionsForVariant')
     getOptionsForVariant(ctx: RequestContext, variantId: ID): Promise<Array<Translated<ProductOption>>> {
         return this.connection
             .findOneInChannel(ctx, ProductVariant, variantId, ctx.channelId, {
@@ -280,6 +290,7 @@ export class ProductVariantService {
             .then(variant => (!variant ? [] : variant.options.map(o => this.translator.translate(o, ctx))));
     }
 
+    @Span('ProductVariantService.getFacetValuesForVariant')
     getFacetValuesForVariant(ctx: RequestContext, variantId: ID): Promise<Array<Translated<FacetValue>>> {
         return this.connection
             .findOneInChannel(ctx, ProductVariant, variantId, ctx.channelId, {
@@ -296,6 +307,7 @@ export class ProductVariantService {
      * method performs a large multi-table join with all the typical data needed for a "product detail"
      * page, this method returns only the Product itself.
      */
+    @Span('ProductVariantService.getProductForVariant')
     async getProductForVariant(ctx: RequestContext, variant: ProductVariant): Promise<Translated<Product>> {
         let product;
 
@@ -316,6 +328,7 @@ export class ProductVariantService {
      * for purchase by Customers. This is determined by the ProductVariant's `stockOnHand` value,
      * as well as the local and global `outOfStockThreshold` settings.
      */
+    @Span('ProductVariantService.getSaleableStockLevel')
     async getSaleableStockLevel(ctx: RequestContext, variant: ProductVariant): Promise<number> {
         const { outOfStockThreshold, trackInventory } = await this.globalSettingsService.getSettings(ctx);
 
@@ -354,6 +367,7 @@ export class ProductVariantService {
      * Returns the stockLevel to display to the customer, as specified by the configured
      * {@link StockDisplayStrategy}.
      */
+    @Span('ProductVariantService.getDisplayStockLevel')
     async getDisplayStockLevel(ctx: RequestContext, variant: ProductVariant): Promise<string> {
         const { stockDisplayStrategy } = this.configService.catalogOptions;
         const saleableStockLevel = await this.getSaleableStockLevel(ctx, variant);
@@ -365,6 +379,7 @@ export class ProductVariantService {
      * Returns the number of fulfillable units of the ProductVariant, equivalent to stockOnHand
      * for those variants which are tracking inventory.
      */
+    @Span('ProductVariantService.getFulfillableStockLevel')
     async getFulfillableStockLevel(ctx: RequestContext, variant: ProductVariant): Promise<number> {
         const { outOfStockThreshold, trackInventory } = await this.globalSettingsService.getSettings(ctx);
         const inventoryNotTracked =
@@ -377,6 +392,7 @@ export class ProductVariantService {
         return stockOnHand;
     }
 
+    @Span('ProductVariantService.create')
     async create(
         ctx: RequestContext,
         input: CreateProductVariantInput[],
@@ -391,6 +407,7 @@ export class ProductVariantService {
         return createdVariants;
     }
 
+    @Span('ProductVariantService.update')
     async update(
         ctx: RequestContext,
         input: UpdateProductVariantInput[],
@@ -406,6 +423,7 @@ export class ProductVariantService {
         return updatedVariants;
     }
 
+    @Span('ProductVariantService.createSingle')
     private async createSingle(ctx: RequestContext, input: CreateProductVariantInput): Promise<ID> {
         await this.validateVariantOptionIds(ctx, input.productId, input.optionIds);
         if (!input.optionIds) {
@@ -473,6 +491,7 @@ export class ProductVariantService {
         return createdVariant.id;
     }
 
+    @Span('ProductVariantService.updateSingle')
     private async updateSingle(ctx: RequestContext, input: UpdateProductVariantInput): Promise<ID> {
         const existingVariant = await this.connection.getEntityOrThrow(ctx, ProductVariant, input.id, {
             channelId: ctx.channelId,
@@ -566,6 +585,7 @@ export class ProductVariantService {
      * Creates a {@link ProductVariantPrice} for the given ProductVariant/Channel combination.
      * If the `currencyCode` is not specified, the default currency of the Channel will be used.
      */
+    @Span('ProductVariantService.createOrUpdateProductVariantPrice')
     async createOrUpdateProductVariantPrice(
         ctx: RequestContext,
         productVariantId: ID,
@@ -639,7 +659,7 @@ export class ProductVariantService {
                 // We don't save the targetPrice again unless it has been assigned
                 // a different price by the ProductVariantPriceUpdateStrategy.
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                !(idsAreEqual(p.id, targetPrice!.id) && p.price === targetPrice!.price),
+                !(idsAreEqual(p.id, targetPrice.id) && p.price === targetPrice.price),
         );
         if (uniqueAdditionalPricesToUpdate.length) {
             const updatedAdditionalPrices = await this.connection
@@ -652,6 +672,7 @@ export class ProductVariantService {
         return targetPrice;
     }
 
+    @Span('ProductVariantService.deleteProductVariantPrice')
     async deleteProductVariantPrice(
         ctx: RequestContext,
         variantId: ID,
@@ -690,6 +711,7 @@ export class ProductVariantService {
         }
     }
 
+    @Span('ProductVariantService.softDelete')
     async softDelete(ctx: RequestContext, id: ID | ID[]): Promise<DeletionResponse> {
         const ids = Array.isArray(id) ? id : [id];
         const variants = await this.connection
@@ -713,6 +735,7 @@ export class ProductVariantService {
      *
      * Is optimized to make as few DB calls as possible using caching based on the open request.
      */
+    @Span('ProductVariantService.hydratePriceFields')
     async hydratePriceFields<F extends 'currencyCode' | 'price' | 'priceWithTax' | 'taxRateApplied'>(
         ctx: RequestContext,
         variant: ProductVariant,
@@ -759,6 +782,7 @@ export class ProductVariantService {
      * Given an array of ProductVariants from the database, this method will apply the correct price and tax
      * and translate each item.
      */
+    @Span('ProductVariantService.applyPricesAndTranslateVariants')
     private async applyPricesAndTranslateVariants(
         ctx: RequestContext,
         variants: ProductVariant[],
@@ -779,6 +803,7 @@ export class ProductVariantService {
      * @description
      * Populates the `price` field with the price for the specified channel.
      */
+    @Span('ProductVariantService.applyChannelPriceAndTax')
     async applyChannelPriceAndTax(
         variant: ProductVariant,
         ctx: RequestContext,
@@ -793,6 +818,7 @@ export class ProductVariantService {
      * Assigns the specified ProductVariants to the specified Channel. In doing so, it will create a new
      * {@link ProductVariantPrice} and also assign the associated Product and any Assets to the Channel too.
      */
+    @Span('ProductVariantService.assignProductVariantsToChannel')
     async assignProductVariantsToChannel(
         ctx: RequestContext,
         input: AssignProductVariantsToChannelInput,
@@ -843,6 +869,7 @@ export class ProductVariantService {
         return result;
     }
 
+    @Span('vendure.product-variant-service.remove-product-variants-from-channel')
     async removeProductVariantsFromChannel(
         ctx: RequestContext,
         input: RemoveProductVariantsFromChannelInput,
@@ -899,6 +926,7 @@ export class ProductVariantService {
         return result;
     }
 
+    @Span('vendure.product-variant-service.validate-variant-option-ids')
     private async validateVariantOptionIds(
         ctx: RequestContext,
         productId: ID,
@@ -963,6 +991,7 @@ export class ProductVariantService {
             .join(glue);
     }
 
+    @Span('vendure.product-variant-service.get-tax-category-for-new-variant')
     private async getTaxCategoryForNewVariant(
         ctx: RequestContext,
         taxCategoryId: ID | null | undefined,

+ 103 - 10
packages/core/src/service/services/product.service.ts

@@ -5,13 +5,13 @@ import {
     DeletionResponse,
     DeletionResult,
     ProductFilterParameter,
-    ProductListOptions,
     RemoveOptionGroupFromProductResult,
     RemoveProductsFromChannelInput,
     UpdateProductInput,
 } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
+import { getActiveSpan } from '@vendure/telemetry';
 import { FindOptionsUtils, In, IsNull } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -25,14 +25,15 @@ import { assertFound, idsAreEqual } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Channel } from '../../entity/channel/channel.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
-import { ProductTranslation } from '../../entity/product/product-translation.entity';
-import { Product } from '../../entity/product/product.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { ProductTranslation } from '../../entity/product/product-translation.entity';
+import { Product } from '../../entity/product/product.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { ProductChannelEvent } from '../../event-bus/events/product-channel-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
 import { ProductOptionGroupChangeEvent } from '../../event-bus/events/product-option-group-change-event';
+import { Span } from '../../instrumentation';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { SlugValidator } from '../helpers/slug-validator/slug-validator';
@@ -70,11 +71,15 @@ export class ProductService {
         private productOptionGroupService: ProductOptionGroupService,
     ) {}
 
+    @Span('ProductService.findAll')
     async findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Product>,
         relations?: RelationPaths<Product>,
     ): Promise<PaginatedList<Translated<Product>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('channelId', ctx.channelId.toString());
+
         const effectiveRelations = relations || this.relations.slice();
         const customPropertyMap: { [name: string]: string } = {};
         const hasFacetValueIdFilter = this.listQueryBuilder.filterObjectHasProperty<ProductFilterParameter>(
@@ -93,6 +98,10 @@ export class ProductService {
             effectiveRelations.push('variants');
             customPropertyMap.sku = 'variants.sku';
         }
+
+        span?.setAttribute('hasFacetValueIdFilter', hasFacetValueIdFilter.toString());
+        span?.setAttribute('hasSkuFilter', hasSkuFilter.toString());
+
         return this.listQueryBuilder
             .build(Product, options, {
                 relations: effectiveRelations,
@@ -106,6 +115,8 @@ export class ProductService {
                 const items = products.map(product =>
                     this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]),
                 );
+                span?.setAttribute('productsCount', products.length.toString());
+                span?.setAttribute('totalItems', totalItems.toString());
                 return {
                     items,
                     totalItems,
@@ -113,11 +124,16 @@ export class ProductService {
             });
     }
 
+    @Span('ProductService.findOne')
     async findOne(
         ctx: RequestContext,
         productId: ID,
         relations?: RelationPaths<Product>,
     ): Promise<Translated<Product> | undefined> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+        span?.setAttribute('channelId', ctx.channelId.toString());
+
         const effectiveRelations = relations ?? this.relations.slice();
         if (relations && effectiveRelations.includes('facetValues')) {
             // We need the facet to determine with the FacetValues are public
@@ -131,16 +147,23 @@ export class ProductService {
             },
         });
         if (!product) {
+            span?.setAttribute('found', 'false');
             return;
         }
+        span?.setAttribute('found', 'true');
         return this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]);
     }
 
+    @Span('ProductService.findByIds')
     async findByIds(
         ctx: RequestContext,
         productIds: ID[],
         relations?: RelationPaths<Product>,
     ): Promise<Array<Translated<Product>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productIds', productIds.join(','));
+        span?.setAttribute('channelId', ctx.channelId.toString());
+
         const qb = this.connection
             .getRepository(ctx, Product)
             .createQueryBuilder('product')
@@ -153,42 +176,63 @@ export class ProductService {
             .andWhere('product.id IN (:...ids)', { ids: productIds })
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .getMany()
-            .then(products =>
-                products.map(product =>
+            .then(products => {
+                span?.setAttribute('foundCount', products.length.toString());
+                return products.map(product =>
                     this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]),
-                ),
-            );
+                );
+            });
     }
 
     /**
      * @description
      * Returns all Channels to which the Product is assigned.
      */
+    @Span('ProductService.getProductChannels')
     async getProductChannels(ctx: RequestContext, productId: ID): Promise<Channel[]> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+
         const product = await this.connection.getEntityOrThrow(ctx, Product, productId, {
             relations: ['channels'],
             channelId: ctx.channelId,
         });
+        span?.setAttribute('channelsCount', product.channels.length.toString());
         return product.channels;
     }
 
+    @Span('ProductService.getFacetValuesForProduct')
     getFacetValuesForProduct(ctx: RequestContext, productId: ID): Promise<Array<Translated<FacetValue>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+
         return this.connection
             .getRepository(ctx, Product)
             .findOne({
                 where: { id: productId },
                 relations: ['facetValues'],
             })
-            .then(variant =>
-                !variant ? [] : variant.facetValues.map(o => this.translator.translate(o, ctx, ['facet'])),
-            );
+            .then(product => {
+                if (!product) {
+                    span?.setAttribute('found', 'false');
+                    return [];
+                }
+                span?.setAttribute('found', 'true');
+                span?.setAttribute('facetValuesCount', product.facetValues.length.toString());
+                return product.facetValues.map(o => this.translator.translate(o, ctx, ['facet']));
+            });
     }
 
+    @Span('ProductService.findOneBySlug')
     async findOneBySlug(
         ctx: RequestContext,
         slug: string,
         relations?: RelationPaths<Product>,
     ): Promise<Translated<Product> | undefined> {
+        const span = getActiveSpan();
+        span?.setAttribute('slug', slug);
+        span?.setAttribute('channelId', ctx.channelId.toString());
+
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
         const translationQb = this.connection
             .getRepository(ctx, ProductTranslation)
@@ -212,13 +256,19 @@ export class ProductService {
         // all the joins etc.
         const result = await qb.getRawOne();
         if (result) {
+            span?.setAttribute('found', 'true');
             return this.findOne(ctx, result.id, relations);
         } else {
+            span?.setAttribute('found', 'false');
             return undefined;
         }
     }
 
+    @Span('ProductService.create')
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productName', input.translations?.[0]?.name || 'unnamed');
+
         await this.slugValidator.validateSlugs(ctx, input, ProductTranslation);
         const product = await this.translatableSaver.create({
             ctx,
@@ -236,10 +286,16 @@ export class ProductService {
         await this.customFieldRelationService.updateRelations(ctx, Product, input, product);
         await this.assetService.updateEntityAssets(ctx, product, input);
         await this.eventBus.publish(new ProductEvent(ctx, product, 'created', input));
+
+        span?.setAttribute('newProductId', product.id.toString());
         return assertFound(this.findOne(ctx, product.id));
     }
 
+    @Span('ProductService.update')
     async update(ctx: RequestContext, input: UpdateProductInput): Promise<Translated<Product>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', input.id.toString());
+
         const product = await this.connection.getEntityOrThrow(ctx, Product, input.id, {
             channelId: ctx.channelId,
             relations: ['facetValues', 'facetValues.channels'],
@@ -266,10 +322,15 @@ export class ProductService {
         });
         await this.customFieldRelationService.updateRelations(ctx, Product, input, updatedProduct);
         await this.eventBus.publish(new ProductEvent(ctx, updatedProduct, 'updated', input));
+
         return assertFound(this.findOne(ctx, updatedProduct.id));
     }
 
+    @Span('ProductService.softDelete')
     async softDelete(ctx: RequestContext, productId: ID): Promise<DeletionResponse> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+
         const product = await this.connection.getEntityOrThrow(ctx, Product, productId, {
             relationLoadStrategy: 'query',
             loadEagerRelations: false,
@@ -301,6 +362,7 @@ export class ProductService {
                 }
             }
         }
+        span?.setAttribute('result', DeletionResult.DELETED);
         return {
             result: DeletionResult.DELETED,
         };
@@ -314,10 +376,16 @@ export class ProductService {
      * Internally, this method will also call {@link ProductVariantService} `assignProductVariantsToChannel()` for
      * each of the Product's variants, and will assign the Product's Assets to the Channel too.
      */
+    @Span('ProductService.assignProductsToChannel')
     async assignProductsToChannel(
         ctx: RequestContext,
         input: AssignProductsToChannelInput,
     ): Promise<Array<Translated<Product>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productIds', input.productIds.join(','));
+        span?.setAttribute('channelId', input.channelId.toString());
+        span?.setAttribute('productsCount', input.productIds.length.toString());
+
         const productsWithVariants = await this.connection.getRepository(ctx, Product).find({
             where: { id: In(input.productIds) },
             relations: ['variants', 'assets'],
@@ -345,10 +413,16 @@ export class ProductService {
         );
     }
 
+    @Span('ProductService.removeProductsFromChannel')
     async removeProductsFromChannel(
         ctx: RequestContext,
         input: RemoveProductsFromChannelInput,
     ): Promise<Array<Translated<Product>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productIds', input.productIds.join(','));
+        span?.setAttribute('channelId', input.channelId.toString());
+        span?.setAttribute('productsCount', input.productIds.length.toString());
+
         const productsWithVariants = await this.connection.getRepository(ctx, Product).find({
             where: { id: In(input.productIds) },
             relations: ['variants'],
@@ -371,11 +445,16 @@ export class ProductService {
         );
     }
 
+    @Span('ProductService.addOptionGroupToProduct')
     async addOptionGroupToProduct(
         ctx: RequestContext,
         productId: ID,
         optionGroupId: ID,
     ): Promise<Translated<Product>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+        span?.setAttribute('optionGroupId', optionGroupId.toString());
+
         const product = await this.getProductWithOptionGroups(ctx, productId);
         const optionGroup = await this.connection.getRepository(ctx, ProductOptionGroup).findOne({
             where: { id: optionGroupId },
@@ -405,12 +484,18 @@ export class ProductService {
         return assertFound(this.findOne(ctx, productId));
     }
 
+    @Span('ProductService.removeOptionGroupFromProduct')
     async removeOptionGroupFromProduct(
         ctx: RequestContext,
         productId: ID,
         optionGroupId: ID,
         force?: boolean,
     ): Promise<ErrorResultUnion<RemoveOptionGroupFromProductResult, Translated<Product>>> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+        span?.setAttribute('optionGroupId', optionGroupId.toString());
+        span?.setAttribute('force', Boolean(force).toString());
+
         const product = await this.getProductWithOptionGroups(ctx, productId);
         const optionGroup = product.optionGroups.find(g => idsAreEqual(g.id, optionGroupId));
         if (!optionGroup) {
@@ -455,7 +540,11 @@ export class ProductService {
         return assertFound(this.findOne(ctx, productId));
     }
 
+    @Span('ProductService.getProductWithOptionGroups')
     private async getProductWithOptionGroups(ctx: RequestContext, productId: ID): Promise<Product> {
+        const span = getActiveSpan();
+        span?.setAttribute('productId', productId.toString());
+
         const product = await this.connection.getRepository(ctx, Product).findOne({
             relationLoadStrategy: 'query',
             loadEagerRelations: false,
@@ -463,8 +552,12 @@ export class ProductService {
             relations: ['optionGroups', 'variants', 'variants.options'],
         });
         if (!product) {
+            span?.setAttribute('found', 'false');
             throw new EntityNotFoundError('Product', productId);
         }
+        span?.setAttribute('found', 'true');
+        span?.setAttribute('optionGroupsCount', product.optionGroups.length.toString());
+        span?.setAttribute('variantsCount', product.variants.length.toString());
         return product;
     }
 }

+ 0 - 1
packages/dev-server/dev-config.ts

@@ -63,7 +63,6 @@ export const devConfig: VendureConfig = {
     paymentOptions: {
         paymentMethodHandlers: [dummyPaymentHandler],
     },
-
     customFields: {
         Product: [
             {

+ 3 - 39
packages/dev-server/instrumentation.ts

@@ -1,49 +1,13 @@
-import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
 import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
-import { Instrumentation } from '@opentelemetry/instrumentation';
-import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
-import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
-import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
-import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
-import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
-import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
-import { resourceFromAttributes } from '@opentelemetry/resources';
-import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
 import { NodeSDK } from '@opentelemetry/sdk-node';
-import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
-import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
+import { getSdkConfiguration } from '@vendure/telemetry';
 
 const traceExporter = new OTLPTraceExporter({
     url: 'http://localhost:4318/v1/traces',
 });
 
-const metricExporter = new OTLPMetricExporter({
-    url: 'http://localhost:4318/v1/metrics',
-});
-
-const instrumentations: Instrumentation[] = [];
-
-const httpInstrumentation = new HttpInstrumentation();
-const expressInstrumentation = new ExpressInstrumentation();
-const graphqlInstrumentation = new GraphQLInstrumentation({
-    mergeItems: true,
-});
-
-instrumentations.push(new NestInstrumentation());
-instrumentations.push(...[httpInstrumentation, expressInstrumentation, graphqlInstrumentation]);
-instrumentations.push(new PgInstrumentation());
-instrumentations.push(new IORedisInstrumentation());
+const config = getSdkConfiguration(true, true);
 
-const sdk = new NodeSDK({
-    spanProcessors: [new SimpleSpanProcessor(traceExporter)],
-    metricReader: new PeriodicExportingMetricReader({
-        exporter: metricExporter,
-    }),
-    resource: resourceFromAttributes({
-        [ATTR_SERVICE_NAME]: 'vendure-local-dev-server',
-        [ATTR_SERVICE_VERSION]: '1.0',
-    }),
-    instrumentations,
-});
+const sdk = new NodeSDK(config);
 
 sdk.start();

+ 0 - 6
packages/dev-server/package.json

@@ -16,12 +16,6 @@
     },
     "dependencies": {
         "@nestjs/axios": "^4.0.0",
-        "@opentelemetry/instrumentation-express": "^0.48.0",
-        "@opentelemetry/instrumentation-graphql": "^0.48.0",
-        "@opentelemetry/instrumentation-http": "^0.200.0",
-        "@opentelemetry/instrumentation-ioredis": "^0.48.0",
-        "@opentelemetry/instrumentation-nestjs-core": "^0.46.0",
-        "@opentelemetry/instrumentation-pg": "^0.52.0",
         "@vendure/admin-ui-plugin": "3.2.2",
         "@vendure/asset-server-plugin": "3.2.2",
         "@vendure/common": "3.2.2",

+ 37 - 0
packages/telemetry/package.json

@@ -0,0 +1,37 @@
+{
+    "name": "@vendure/telemetry",
+    "version": "3.2.2",
+    "description": "Telemetry for Vendure",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/vendure-ecommerce/vendure/"
+    },
+    "homepage": "https://www.vendure.io",
+    "funding": "https://github.com/sponsors/michaelbromley",
+    "private": false,
+    "license": "GPL-3.0-or-later",
+    "type": "commonjs",
+    "scripts": {
+        "build": "rimraf dist && tsc -p ./tsconfig.build.json",
+        "watch": "tsc -p ./tsconfig.build.json --watch"
+    },
+    "publishConfig": {
+        "access": "public"
+    },
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "files": [
+        "dist/**/*"
+    ],
+    "dependencies": {
+        "@opentelemetry/auto-instrumentations-node": "^0.58.0",
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/resources": "^2.0.0",
+        "@opentelemetry/sdk-node": "^0.200.0",
+        "@opentelemetry/context-async-hooks": "^2.0.0",
+        "@vendure/common": "3.2.2"
+    },
+    "devDependencies": {
+        "typescript": "5.8.2"
+    }
+}

+ 43 - 0
packages/telemetry/src/decorator/span.ts

@@ -0,0 +1,43 @@
+// packages/tracing-utils/createSpanDecorator.ts
+import { context, SpanStatusCode, trace, Tracer } from '@opentelemetry/api';
+
+/**
+ * @description
+ * Returns a `@Span()` method decorator bound to the given tracer.
+ *
+ * Usage in each package:
+ *   export const Span = createSpanDecorator(tracer);
+ *
+ * @since 3.3.0
+ */
+export function createSpanDecorator(tracer: Tracer) {
+    return function Span(name?: string): MethodDecorator {
+        return (target, propertyKey, descriptor: PropertyDescriptor) => {
+            // Original method or handler
+            const original = descriptor.value;
+
+            // Default to the method name if none supplied
+            const spanName = name ?? String(propertyKey);
+
+            descriptor.value = function (...args: unknown[]) {
+                const span = tracer.startSpan(spanName);
+
+                // Make the new span the *active* span for anything called inside
+                return context.with(trace.setSpan(context.active(), span), async () => {
+                    try {
+                        // Support sync & async transparently
+                        const result = await original.apply(this, args);
+                        span.setStatus({ code: SpanStatusCode.OK });
+                        return result;
+                    } catch (err) {
+                        span.recordException(err as Error);
+                        span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
+                        throw err;
+                    } finally {
+                        span.end();
+                    }
+                });
+            };
+        };
+    };
+}

+ 5 - 0
packages/telemetry/src/index.ts

@@ -0,0 +1,5 @@
+export * from './decorator/span';
+export * from './instrumentation';
+export * from './tracing.plugin';
+export * from './tracing/trace.service';
+export * from './utils/span';

+ 35 - 0
packages/telemetry/src/instrumentation.ts

@@ -0,0 +1,35 @@
+import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
+import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
+import { resourceFromAttributes } from '@opentelemetry/resources';
+import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
+import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
+import { VENDURE_VERSION } from '@vendure/core';
+
+export function getSdkConfiguration(
+    instrumentationEnabled: boolean = true,
+    devMode: boolean = false,
+    config: Partial<NodeSDKConfiguration> = {},
+): Partial<NodeSDKConfiguration> {
+    const { spanProcessors, ...rest } = config;
+
+    const devModeAwareConfig: Partial<NodeSDKConfiguration> = devMode
+        ? {
+              spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
+          }
+        : {
+              spanProcessors,
+          };
+
+    return {
+        resource: resourceFromAttributes({
+            'service.name': 'vendure',
+            'service.version': VENDURE_VERSION,
+            'service.namespace': 'vendure',
+            'service.environment': process.env.NODE_ENV || 'development',
+        }),
+        ...devModeAwareConfig,
+        contextManager: new AsyncLocalStorageContextManager(),
+        instrumentations: instrumentationEnabled ? [getNodeAutoInstrumentations()] : [],
+        ...rest,
+    };
+}

+ 17 - 0
packages/telemetry/src/tracing.plugin.ts

@@ -0,0 +1,17 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { TraceService } from './tracing/trace.service';
+import { TracingPluginOptions } from './types';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [TraceService],
+    exports: [TraceService],
+})
+export class TracingPlugin {
+    static options: TracingPluginOptions;
+
+    static init(options: TracingPluginOptions) {
+        this.options = options;
+    }
+}

+ 13 - 0
packages/telemetry/src/tracing/trace.service.ts

@@ -0,0 +1,13 @@
+import { Injectable } from '@nestjs/common';
+import { context, Span, trace, Tracer } from '@opentelemetry/api';
+
+@Injectable()
+export class TraceService {
+    public getSpan(): Span | undefined {
+        return trace.getSpan(context.active());
+    }
+
+    public startSpan(tracer: Tracer, name: string): Span {
+        return tracer.startSpan(name);
+    }
+}

+ 1 - 0
packages/telemetry/src/types.ts

@@ -0,0 +1 @@
+export interface TracingPluginOptions {}

+ 5 - 0
packages/telemetry/src/utils/span.ts

@@ -0,0 +1,5 @@
+import { context, Span, trace } from '@opentelemetry/api';
+
+export function getActiveSpan(): Span | undefined {
+    return trace.getSpan(context.active());
+}

+ 7 - 0
packages/telemetry/tsconfig.build.json

@@ -0,0 +1,7 @@
+{
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "outDir": "./dist"
+    },
+    "files": ["./src/index.ts"]
+}

+ 13 - 0
packages/telemetry/tsconfig.json

@@ -0,0 +1,13 @@
+{
+    "extends": "../../tsconfig.json",
+    "compilerOptions": {
+        "module": "NodeNext",
+        "moduleResolution": "NodeNext",
+        "declaration": true,
+        "removeComments": true,
+        "strictPropertyInitialization": false,
+        "sourceMap": true,
+        "allowJs": true
+    },
+    "exclude": ["node_modules"]
+}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff