diff --git a/__mocks__/@vtex/diagnostics-semconv.ts b/__mocks__/@vtex/diagnostics-semconv.ts index 29a85bedb..8f4ecece1 100644 --- a/__mocks__/@vtex/diagnostics-semconv.ts +++ b/__mocks__/@vtex/diagnostics-semconv.ts @@ -4,6 +4,8 @@ const ATTR_VTEX_IO_WORKSPACE_NAME = 'vtex_io.workspace.name' const ATTR_VTEX_IO_WORKSPACE_TYPE = 'vtex_io.workspace.type' const ATTR_VTEX_IO_APP_ID = 'vtex_io.app.id' const ATTR_VTEX_IO_APP_AUTHOR_TYPE = 'vtex_io.app.author-type' +const ATTR_VTEX_IO_CLUSTER_ID = 'vtex_io.cluster.id' +const ATTR_VTEX_IO_CLUSTER_ROLE = 'vtex_io.cluster.role' export { ATTR_VTEX_ACCOUNT_NAME, @@ -11,4 +13,6 @@ export { ATTR_VTEX_IO_WORKSPACE_TYPE, ATTR_VTEX_IO_APP_ID, ATTR_VTEX_IO_APP_AUTHOR_TYPE, + ATTR_VTEX_IO_CLUSTER_ID, + ATTR_VTEX_IO_CLUSTER_ROLE, } diff --git a/openspec/changes/add-k8s-cluster-telemetry-dimensions/.openspec.yaml b/openspec/changes/add-k8s-cluster-telemetry-dimensions/.openspec.yaml new file mode 100644 index 000000000..9e5b8a190 --- /dev/null +++ b/openspec/changes/add-k8s-cluster-telemetry-dimensions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-23 diff --git a/openspec/changes/add-k8s-cluster-telemetry-dimensions/design.md b/openspec/changes/add-k8s-cluster-telemetry-dimensions/design.md new file mode 100644 index 000000000..30625448e --- /dev/null +++ b/openspec/changes/add-k8s-cluster-telemetry-dimensions/design.md @@ -0,0 +1,61 @@ +## Context + +The diagnostics telemetry stack is initialized once in `src/service/telemetry/client.ts`. `NewTelemetryClient` receives `additionalAttrs`, which become resource attributes shared by the metrics, logs, and traces clients created from that telemetry client. Today those attributes identify the IO app, vendor, version, and workspace, while signal-specific code adds request and operation dimensions. + +Cluster metadata is deployment-level rather than request-level. Adding it independently at every metric and log call site would duplicate logic, miss automatic or future instrumentation, and consume the custom metric attribute budget enforced by `DiagnosticsMetrics`. + +## Goals / Non-Goals + +**Goals:** + +- Export the authoritative Kubernetes cluster identifier/name as `vtex_io.cluster.id`. +- Export the cluster's platform role as `vtex_io.cluster.role`. +- Make both dimensions available on every diagnostics metric and log, including automatically instrumented telemetry. +- Preserve current behavior when either metadata value is unavailable. +- Keep cluster metadata low-cardinality and constant for the process lifetime. + +**Non-Goals:** + +- Deriving cluster identity from hostnames, pod names, request headers, or network calls. +- Adding pod, node, namespace, or workload dimensions. +- Renaming existing metrics, changing log messages, or modifying legacy console-based telemetry. +- Requiring cluster metadata when running locally or outside Kubernetes. + +## Decisions + +### Add cluster metadata as telemetry resource attributes + +The implementation will extend the `additionalAttrs` passed to `NewTelemetryClient` with non-empty `vtex_io.cluster.id` and `vtex_io.cluster.role` values. This is the single initialization point shared by diagnostics metrics and logs and covers direct instruments, the `DiagnosticsMetrics` wrapper, host metrics, and logger calls. + +Per-call instrumentation was rejected because the repository has several independent metric paths and automatic instrumentation. Updating every call site would be error-prone and would count the values against the seven custom attributes accepted by `DiagnosticsMetrics`. + +Because the diagnostics clients share one OpenTelemetry resource, traces may also carry these attributes. That consistency is accepted as a side effect; metrics and logs are the required signals. + +### Treat runtime metadata as authoritative configuration + +The cluster identifier/name will be read from `process.env.VTEX_CLUSTER_ID`, and the cluster role will be read from `process.env.VTEX_CLUSTER_ROLE`. Both values will be normalized by trimming whitespace. Empty, whitespace-only, or undefined values will be omitted independently. `VTEX_REGION` will not be used as a fallback because region and cluster identity have different semantics. + +Fallback strings such as `unknown` were rejected because they create an artificial cluster that combines unrelated local or misconfigured workloads. + +### Use generated VTEX IO semantic-convention keys + +`AttributeKeys.VTEX_IO_CLUSTER_ID` and `AttributeKeys.VTEX_IO_CLUSTER_ROLE` will reference `ATTR_VTEX_IO_CLUSTER_ID` and `ATTR_VTEX_IO_CLUSTER_ROLE` from `@vtex/diagnostics-semconv`, following the existing workspace and app attribute pattern. These generated constants resolve to the stable `vtex_io.cluster.id` and `vtex_io.cluster.role` keys introduced by `vtex/diagnostics#174`. + +Hard-coded local attribute names were rejected because they would duplicate the semantic-convention package and could drift from the cross-language contract. + +## Risks / Trade-offs + +- **A runtime does not inject the cluster variables** → Treat both variables as optional and add tests around the resulting constants. +- **Resource attributes also appear on traces** → Accept this because the telemetry client shares a resource and consistent deployment identity is useful across signals. +- **Additional dimensions increase metric series count** → Cluster identity and role are bounded deployment metadata; do not add pod- or request-level values. +- **A deployment omits one value** → Emit the available dimension independently and omit only the missing one. +- **The semantic-convention upgrade introduces a major-version change** → Pin version `5.5.2` and validate the full build, lint, and test suites. +- **A caller emits a data-point attribute with the same key** → Treat `vtex_io.cluster.id` and `vtex_io.cluster.role` as platform resource dimensions and test the diagnostics payload shape at initialization. + +## Migration Plan + +1. Update `@vtex/diagnostics-semconv` to `5.5.2`, the first public version adopted here that contains both cluster constants. +2. Validate that build, lint, and tests pass with the published package. +3. Deploy without changing existing metric names or log schemas beyond the two optional dimensions. +4. Verify emitted metrics and logs in one development cluster before broad rollout. +5. Roll back by reverting the resource attributes; no stored-data migration is required. diff --git a/openspec/changes/add-k8s-cluster-telemetry-dimensions/proposal.md b/openspec/changes/add-k8s-cluster-telemetry-dimensions/proposal.md new file mode 100644 index 000000000..41cc70a36 --- /dev/null +++ b/openspec/changes/add-k8s-cluster-telemetry-dimensions/proposal.md @@ -0,0 +1,27 @@ +## Why + +Metrics and logs emitted through the diagnostics API cannot currently be grouped or filtered by the Kubernetes cluster that produced them. Adding stable cluster identity and role dimensions will make cross-cluster comparison, incident isolation, and dashboard segmentation possible. + +## What Changes + +- Read the Kubernetes cluster identifier/name from `VTEX_CLUSTER_ID` and cluster role from `VTEX_CLUSTER_ROLE`. +- Attach the values as the stable `vtex_io.cluster.id` and `vtex_io.cluster.role` semantic-convention dimensions from `@vtex/diagnostics-semconv`. +- Apply the dimensions centrally so built-in and application-provided telemetry receive the same metadata. +- Omit unavailable or empty cluster metadata rather than emitting misleading placeholder values. +- Add coverage for configured and missing cluster metadata. + +## Capabilities + +### New Capabilities +- `cluster-telemetry-dimensions`: Defines how Kubernetes cluster identity and role are represented on diagnostics metrics and logs. + +### Modified Capabilities + +None. + +## Impact + +- Telemetry initialization in `src/service/telemetry/client.ts`. +- Diagnostics metric and log tests, constants for `VTEX_CLUSTER_ID` and `VTEX_CLUSTER_ROLE`, and adoption of the cluster attributes introduced by `vtex/diagnostics#174`. +- Metrics and logs gain two low-cardinality dimensions; dashboards and queries can adopt them without changing metric names or log messages. +- The deployment/runtime contract exposes the authoritative Kubernetes cluster identifier/name and role through `VTEX_CLUSTER_ID` and `VTEX_CLUSTER_ROLE`. diff --git a/openspec/changes/add-k8s-cluster-telemetry-dimensions/specs/cluster-telemetry-dimensions/spec.md b/openspec/changes/add-k8s-cluster-telemetry-dimensions/specs/cluster-telemetry-dimensions/spec.md new file mode 100644 index 000000000..05dd04541 --- /dev/null +++ b/openspec/changes/add-k8s-cluster-telemetry-dimensions/specs/cluster-telemetry-dimensions/spec.md @@ -0,0 +1,54 @@ +## ADDED Requirements + +### Requirement: Diagnostics metrics include Kubernetes cluster dimensions +The system SHALL attach configured Kubernetes cluster metadata to every metric emitted through the diagnostics telemetry client. The Kubernetes cluster identifier or name SHALL be exported as `vtex_io.cluster.id`, and the cluster role SHALL be exported as `vtex_io.cluster.role`. + +#### Scenario: Both cluster values are configured for metrics +- **WHEN** diagnostics telemetry initializes with non-empty cluster identifier and cluster role metadata +- **THEN** every diagnostics metric has `vtex_io.cluster.id` and `vtex_io.cluster.role` resource dimensions containing the configured values + +#### Scenario: Automatically instrumented metric is emitted +- **WHEN** an automatic or built-in diagnostics metric is emitted after telemetry initialization +- **THEN** the metric has the same `vtex_io.cluster.id` and `vtex_io.cluster.role` resource dimensions as an application-provided metric + +### Requirement: Diagnostics logs include Kubernetes cluster dimensions +The system SHALL attach configured Kubernetes cluster metadata to every log emitted through the diagnostics telemetry client, using `vtex_io.cluster.id` for the Kubernetes cluster identifier or name and `vtex_io.cluster.role` for the cluster role. + +#### Scenario: Both cluster values are configured for logs +- **WHEN** the diagnostics logger emits a log after telemetry initializes with non-empty cluster identifier and cluster role metadata +- **THEN** the log has `vtex_io.cluster.id` and `vtex_io.cluster.role` resource dimensions containing the configured values + +#### Scenario: Logs and metrics use consistent cluster values +- **WHEN** a diagnostics metric and diagnostics log are emitted by the same process +- **THEN** both signals have identical `vtex_io.cluster.id` and `vtex_io.cluster.role` values + +### Requirement: Missing cluster metadata is omitted safely +The system SHALL normalize cluster metadata by trimming surrounding whitespace and SHALL omit a cluster dimension when its configured value is undefined, empty, or whitespace-only. Availability of one cluster value SHALL NOT depend on availability of the other. + +#### Scenario: No cluster metadata is configured +- **WHEN** diagnostics telemetry initializes without a cluster identifier and cluster role +- **THEN** telemetry initialization succeeds and neither `vtex_io.cluster.id` nor `vtex_io.cluster.role` is emitted + +#### Scenario: Only cluster identifier is configured +- **WHEN** diagnostics telemetry initializes with a non-empty cluster identifier and no cluster role +- **THEN** emitted diagnostics metrics and logs include `vtex_io.cluster.id` and omit `vtex_io.cluster.role` + +#### Scenario: Only cluster role is configured +- **WHEN** diagnostics telemetry initializes with a non-empty cluster role and no cluster identifier +- **THEN** emitted diagnostics metrics and logs include `vtex_io.cluster.role` and omit `vtex_io.cluster.id` + +#### Scenario: Cluster metadata contains surrounding whitespace +- **WHEN** configured cluster metadata contains leading or trailing whitespace +- **THEN** emitted cluster dimensions contain the trimmed values + +### Requirement: Cluster dimensions use deployment metadata +The system MUST obtain cluster identity from `process.env.VTEX_CLUSTER_ID` and cluster role from `process.env.VTEX_CLUSTER_ROLE` and SHALL keep those values constant for the lifetime of the telemetry client. The system SHALL NOT use `VTEX_REGION` as a fallback for cluster identity. The attribute keys SHALL come from `ATTR_VTEX_IO_CLUSTER_ID` and `ATTR_VTEX_IO_CLUSTER_ROLE` exported by `@vtex/diagnostics-semconv`. + +#### Scenario: Telemetry client is initialized +- **WHEN** the process creates its diagnostics telemetry client +- **THEN** it reads cluster identity from `VTEX_CLUSTER_ID` and cluster role from `VTEX_CLUSTER_ROLE` and applies them as resource attributes +- **AND** it uses the generated VTEX IO semantic-convention keys rather than locally defined attribute-name strings + +#### Scenario: Request data differs between telemetry calls +- **WHEN** metrics or logs are emitted for different accounts, workspaces, routes, or operations +- **THEN** their cluster dimensions remain the deployment-level values selected at telemetry initialization diff --git a/openspec/changes/add-k8s-cluster-telemetry-dimensions/tasks.md b/openspec/changes/add-k8s-cluster-telemetry-dimensions/tasks.md new file mode 100644 index 000000000..5f7e01661 --- /dev/null +++ b/openspec/changes/add-k8s-cluster-telemetry-dimensions/tasks.md @@ -0,0 +1,21 @@ +## 1. Runtime Metadata Configuration + +- [x] 1.1 Add constants that read cluster identifier/name from `VTEX_CLUSTER_ID` and cluster role from `VTEX_CLUSTER_ROLE` +- [x] 1.2 Expose `ATTR_VTEX_IO_CLUSTER_ID` and `ATTR_VTEX_IO_CLUSTER_ROLE` through `AttributeKeys`, following the existing semantic-convention pattern + +## 2. Cluster Metadata Configuration + +- [x] 2.1 Add a helper that maps the runtime cluster variables to `vtex_io.cluster.id` and `vtex_io.cluster.role` without using `VTEX_REGION` as a fallback +- [x] 2.2 Add normalization that trims configured values and omits undefined, empty, or whitespace-only metadata independently +- [x] 2.3 Add unit tests covering both values, either value alone, missing values, and whitespace normalization + +## 3. Diagnostics Telemetry Integration + +- [x] 3.1 Add the normalized semantic-convention cluster values conditionally to `NewTelemetryClient` resource attributes +- [x] 3.2 Add telemetry initialization tests proving diagnostics metrics and logs share `vtex_io.cluster.id` and `vtex_io.cluster.role` without changing metric-call custom attributes +- [x] 3.3 Verify initialization remains successful when neither cluster value is available + +## 4. Validation + +- [x] 4.1 Confirm the previous build and focused-test failures were limited to the then-unpublished `ATTR_VTEX_IO_CLUSTER_ID` and `ATTR_VTEX_IO_CLUSTER_ROLE` exports +- [x] 4.2 Update `@vtex/diagnostics-semconv` to `5.5.2` and run repository linting, type checking, and the broader test suite diff --git a/package.json b/package.json index 829761a1e..9660301f7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@types/koa": "^2.11.0", "@types/koa-compose": "^3.2.3", "@vtex/diagnostics-nodejs": "0.1.8-io", - "@vtex/diagnostics-semconv": "^1.1.2", + "@vtex/diagnostics-semconv": "5.5.2", "@vtex/node-error-report": "^0.0.3", "@wry/equality": "^0.1.9", "agentkeepalive": "^4.0.2", diff --git a/src/constants.test.ts b/src/constants.test.ts index c4bc3096c..21aafe65d 100644 --- a/src/constants.test.ts +++ b/src/constants.test.ts @@ -12,6 +12,8 @@ import { MAX_WORKERS, LINKED, REGION, + CLUSTER_ID, + CLUSTER_ROLE, PUBLIC_ENDPOINT, APP, NODE_ENV, @@ -130,11 +132,15 @@ describe('constants', () => { expect(AttributeKeys).toHaveProperty('VTEX_IO_WORKSPACE_TYPE') expect(AttributeKeys).toHaveProperty('VTEX_IO_APP_ID') expect(AttributeKeys).toHaveProperty('VTEX_IO_APP_AUTHOR_TYPE') + expect(AttributeKeys).toHaveProperty('VTEX_IO_CLUSTER_ID') + expect(AttributeKeys).toHaveProperty('VTEX_IO_CLUSTER_ROLE') expect(typeof AttributeKeys.VTEX_IO_WORKSPACE_NAME).toBe('string') expect(typeof AttributeKeys.VTEX_IO_WORKSPACE_TYPE).toBe('string') expect(typeof AttributeKeys.VTEX_IO_APP_ID).toBe('string') expect(typeof AttributeKeys.VTEX_IO_APP_AUTHOR_TYPE).toBe('string') + expect(AttributeKeys.VTEX_IO_CLUSTER_ID).toBe('vtex_io.cluster.id') + expect(AttributeKeys.VTEX_IO_CLUSTER_ROLE).toBe('vtex_io.cluster.role') }) test('should have non-empty string values', () => { @@ -218,6 +224,8 @@ describe('constants', () => { test('string environment constants should match their env vars', () => { expect(REGION).toBe(process.env.VTEX_REGION as string) + expect(CLUSTER_ID).toBe(process.env.VTEX_CLUSTER_ID as string) + expect(CLUSTER_ROLE).toBe(process.env.VTEX_CLUSTER_ROLE as string) expect(NODE_ENV).toBe(process.env.NODE_ENV as string) expect(ACCOUNT).toBe(process.env.VTEX_ACCOUNT as string) expect(WORKSPACE).toBe(process.env.VTEX_WORKSPACE as string) diff --git a/src/constants.ts b/src/constants.ts index 6cdb680fd..112b37bef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,7 +4,9 @@ import { ATTR_VTEX_IO_WORKSPACE_NAME, ATTR_VTEX_IO_WORKSPACE_TYPE, ATTR_VTEX_IO_APP_ID, - ATTR_VTEX_IO_APP_AUTHOR_TYPE + ATTR_VTEX_IO_APP_AUTHOR_TYPE, + ATTR_VTEX_IO_CLUSTER_ID, + ATTR_VTEX_IO_CLUSTER_ROLE, } from '@vtex/diagnostics-semconv' // tslint:disable-next-line @@ -62,6 +64,8 @@ export const AttributeKeys = { VTEX_IO_WORKSPACE_TYPE: ATTR_VTEX_IO_WORKSPACE_TYPE, VTEX_IO_APP_ID: ATTR_VTEX_IO_APP_ID, VTEX_IO_APP_AUTHOR_TYPE: ATTR_VTEX_IO_APP_AUTHOR_TYPE, + VTEX_IO_CLUSTER_ID: ATTR_VTEX_IO_CLUSTER_ID, + VTEX_IO_CLUSTER_ROLE: ATTR_VTEX_IO_CLUSTER_ROLE, } /** @deprecated Use HeaderKeys.CACHE_CONTROL instead */ @@ -165,6 +169,8 @@ export const MAX_WORKERS = 4 export const LINKED = !!process.env.VTEX_APP_LINK export const REGION = process.env.VTEX_REGION as string +export const CLUSTER_ID = process.env.VTEX_CLUSTER_ID as string +export const CLUSTER_ROLE = process.env.VTEX_CLUSTER_ROLE as string export const PUBLIC_ENDPOINT = process.env.VTEX_PUBLIC_ENDPOINT || 'myvtex.com' export const APP = { ID: process.env.VTEX_APP_ID as string, diff --git a/src/service/telemetry/client.test.ts b/src/service/telemetry/client.test.ts new file mode 100644 index 000000000..ce3c5ea31 --- /dev/null +++ b/src/service/telemetry/client.test.ts @@ -0,0 +1,118 @@ +const mockNewTelemetryClient = jest.fn() +const mockCreateExporter = jest.fn() +const mockCreateTracesExporterConfig = jest.fn() +const mockCreateMetricsExporterConfig = jest.fn() +const mockCreateLogsExporterConfig = jest.fn() +const mockGetClusterResourceAttributes = jest.fn() + +jest.mock('@vtex/diagnostics-nodejs', () => ({ + Exporters: { + CreateExporter: mockCreateExporter, + CreateLogsExporterConfig: mockCreateLogsExporterConfig, + CreateMetricsExporterConfig: mockCreateMetricsExporterConfig, + CreateTracesExporterConfig: mockCreateTracesExporterConfig, + }, + Instrumentation: { + CommonInstrumentations: { + minimal: jest.fn(() => []), + }, + }, + NewTelemetryClient: mockNewTelemetryClient, +})) + +jest.mock('../../constants', () => ({ + APP: { + ID: 'vtex.test-app@1.0.0', + VENDOR: 'vtex', + VERSION: '1.0.0', + }, + AttributeKeys: { + VTEX_IO_APP_ID: 'vtex_io.app.id', + VTEX_IO_WORKSPACE_NAME: 'vtex_io.workspace.name', + VTEX_IO_WORKSPACE_TYPE: 'vtex_io.workspace.type', + VTEX_IO_CLUSTER_ID: 'vtex_io.cluster.id', + VTEX_IO_CLUSTER_ROLE: 'vtex_io.cluster.role', + }, + CLUSTER_ID: 'cluster-a', + CLUSTER_ROLE: 'stores', + DIAGNOSTICS_TELEMETRY_ENABLED: false, + DK_APP_ID: 'apps-team', + OTEL_EXPORTER_OTLP_ENDPOINT: 'http://collector', + PRODUCTION: true, + WORKSPACE: 'master', +})) + +jest.mock('../metrics/instruments/hostMetrics', () => ({ + HostMetricsInstrumentation: jest.fn(), +})) + +jest.mock('./resourceAttributes', () => ({ + getClusterResourceAttributes: mockGetClusterResourceAttributes, +})) + +import { initializeTelemetry, resetTelemetry } from './client' + +describe('diagnostics telemetry resource attributes', () => { + const tracesClient = {} + const metricsClient = { provider: jest.fn() } + const logsClient = {} + const telemetryClient = { + newLogsClient: jest.fn(), + newMetricsClient: jest.fn(), + newTracesClient: jest.fn(), + registerInstrumentations: jest.fn(), + } + + beforeEach(() => { + jest.clearAllMocks() + resetTelemetry() + + mockCreateExporter.mockImplementation(config => config) + mockCreateTracesExporterConfig.mockReturnValue({ signal: 'traces' }) + mockCreateMetricsExporterConfig.mockReturnValue({ signal: 'metrics' }) + mockCreateLogsExporterConfig.mockReturnValue({ signal: 'logs' }) + mockNewTelemetryClient.mockResolvedValue(telemetryClient) + telemetryClient.newTracesClient.mockResolvedValue(tracesClient) + telemetryClient.newMetricsClient.mockResolvedValue(metricsClient) + telemetryClient.newLogsClient.mockResolvedValue(logsClient) + }) + + it('shares configured cluster resource attributes across metrics and logs', async () => { + mockGetClusterResourceAttributes.mockReturnValue({ + 'vtex_io.cluster.id': 'cluster-a', + 'vtex_io.cluster.role': 'stores', + }) + + const clients = await initializeTelemetry() + + expect(mockGetClusterResourceAttributes).toHaveBeenCalledWith('cluster-a', 'stores') + expect(mockNewTelemetryClient).toHaveBeenCalledWith( + 'apps-team', + 'node-vtex-api', + 'vtex.test-app@1.0.0', + expect.objectContaining({ + additionalAttrs: expect.objectContaining({ + 'vtex_io.cluster.id': 'cluster-a', + 'vtex_io.cluster.role': 'stores', + }), + }) + ) + expect(telemetryClient.newMetricsClient).toHaveBeenCalledTimes(1) + expect(telemetryClient.newLogsClient).toHaveBeenCalledTimes(1) + expect(telemetryClient.newMetricsClient.mock.calls[0][0]).not.toHaveProperty('additionalAttrs') + expect(telemetryClient.newLogsClient.mock.calls[0][0]).not.toHaveProperty('additionalAttrs') + expect(clients).toEqual({ tracesClient, metricsClient, logsClient }) + }) + + it('initializes without cluster dimensions when metadata is unavailable', async () => { + mockGetClusterResourceAttributes.mockReturnValue({}) + + await initializeTelemetry() + + const options = mockNewTelemetryClient.mock.calls[0][3] + expect(options.additionalAttrs).not.toHaveProperty('vtex_io.cluster.id') + expect(options.additionalAttrs).not.toHaveProperty('vtex_io.cluster.role') + expect(telemetryClient.newMetricsClient).toHaveBeenCalledTimes(1) + expect(telemetryClient.newLogsClient).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/service/telemetry/client.ts b/src/service/telemetry/client.ts index 9ce1471c8..96e044164 100644 --- a/src/service/telemetry/client.ts +++ b/src/service/telemetry/client.ts @@ -8,8 +8,19 @@ import { Traces, } from '@vtex/diagnostics-nodejs' import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry' -import { APP, AttributeKeys, DIAGNOSTICS_TELEMETRY_ENABLED, DK_APP_ID, OTEL_EXPORTER_OTLP_ENDPOINT, PRODUCTION, WORKSPACE } from '../../constants' +import { + APP, + AttributeKeys, + CLUSTER_ID, + CLUSTER_ROLE, + DIAGNOSTICS_TELEMETRY_ENABLED, + DK_APP_ID, + OTEL_EXPORTER_OTLP_ENDPOINT, + PRODUCTION, + WORKSPACE, +} from '../../constants' import { HostMetricsInstrumentation } from '../metrics/instruments/hostMetrics' +import { getClusterResourceAttributes } from './resourceAttributes' const APPLICATION_ID = APP.ID || 'vtex-io-app' @@ -90,6 +101,7 @@ class TelemetryClientSingleton { 'version': APP.VERSION || '', [AttributeKeys.VTEX_IO_WORKSPACE_NAME]: WORKSPACE, [AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: PRODUCTION ? 'production' : 'development', + ...getClusterResourceAttributes(CLUSTER_ID, CLUSTER_ROLE), }, // Use built-in no-op functionality when telemetry is disabled noop: !DIAGNOSTICS_TELEMETRY_ENABLED, diff --git a/src/service/telemetry/resourceAttributes.test.ts b/src/service/telemetry/resourceAttributes.test.ts new file mode 100644 index 000000000..f9d5fff04 --- /dev/null +++ b/src/service/telemetry/resourceAttributes.test.ts @@ -0,0 +1,36 @@ +import { AttributeKeys } from '../../constants' +import { getClusterResourceAttributes } from './resourceAttributes' + +describe('getClusterResourceAttributes', () => { + it('maps both cluster values to reserved resource attributes', () => { + expect(getClusterResourceAttributes('cluster-a', 'stores')).toEqual({ + [AttributeKeys.VTEX_IO_CLUSTER_ID]: 'cluster-a', + [AttributeKeys.VTEX_IO_CLUSTER_ROLE]: 'stores', + }) + }) + + it('includes only the cluster identifier when role is missing', () => { + expect(getClusterResourceAttributes('cluster-a', undefined)).toEqual({ + [AttributeKeys.VTEX_IO_CLUSTER_ID]: 'cluster-a', + }) + }) + + it('includes only the cluster role when identifier is missing', () => { + expect(getClusterResourceAttributes(undefined, 'stores')).toEqual({ + [AttributeKeys.VTEX_IO_CLUSTER_ROLE]: 'stores', + }) + }) + + it('omits missing and empty values', () => { + expect(getClusterResourceAttributes('', undefined)).toEqual({}) + }) + + it('trims values and omits whitespace-only values independently', () => { + expect(getClusterResourceAttributes(' cluster-a ', ' ')).toEqual({ + [AttributeKeys.VTEX_IO_CLUSTER_ID]: 'cluster-a', + }) + expect(getClusterResourceAttributes(' ', ' stores ')).toEqual({ + [AttributeKeys.VTEX_IO_CLUSTER_ROLE]: 'stores', + }) + }) +}) diff --git a/src/service/telemetry/resourceAttributes.ts b/src/service/telemetry/resourceAttributes.ts new file mode 100644 index 000000000..b9390b539 --- /dev/null +++ b/src/service/telemetry/resourceAttributes.ts @@ -0,0 +1,25 @@ +import { AttributeKeys } from '../../constants' + +const normalizeAttribute = (value?: string): string | undefined => { + const normalized = value && value.trim() + return normalized || undefined +} + +export const getClusterResourceAttributes = ( + clusterId?: string, + clusterRole?: string +): Record => { + const attributes: Record = {} + const normalizedClusterId = normalizeAttribute(clusterId) + const normalizedClusterRole = normalizeAttribute(clusterRole) + + if (normalizedClusterId) { + attributes[AttributeKeys.VTEX_IO_CLUSTER_ID] = normalizedClusterId + } + + if (normalizedClusterRole) { + attributes[AttributeKeys.VTEX_IO_CLUSTER_ROLE] = normalizedClusterRole + } + + return attributes +} diff --git a/yarn.lock b/yarn.lock index 6330ccc57..2f8186ad7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1292,10 +1292,10 @@ resolved "https://registry.yarnpkg.com/@vtex/diagnostics-semconv/-/diagnostics-semconv-0.1.0-beta.11.tgz#2ddfff7dffdc1c052d23b335f914de91653d9659" integrity sha512-H3KM5fYAFmcxhlA4wT5iPgWJtgKsumFqGkkxjcA/BSwC5tgSWezN82sZDKvBsVo24EoZxVGgLlsjNw1tsp9U3Q== -"@vtex/diagnostics-semconv@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vtex/diagnostics-semconv/-/diagnostics-semconv-1.1.2.tgz#ed58b4c0f403cf5d9ff5e3d487e959ff9c1802e2" - integrity sha512-CUz58FTeYHC6z5n0qJKcHesJK00ykwDAFKXUaBKjzI166lm/LqMkdPJA8KE2h4RWGDdSaPDIUDdkueSD76oUfw== +"@vtex/diagnostics-semconv@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@vtex/diagnostics-semconv/-/diagnostics-semconv-5.5.2.tgz#87eeaed851b8bea8ef46f1211552324f98e345f6" + integrity sha512-2uQNJVfLjbhay2pq1pVIIrSe7x7rsIyqIVD9z7syez4Sz/lzlwxNvRfJPt+Sg7MeGraK4+/YiSEO5kgpxGPRLg== "@vtex/node-error-report@^0.0.3": version "0.0.3"