Skip to content

Conversation

EmrysMyrddin
Copy link
Collaborator

🚨 IMPORTANT: Please do not create a Pull Request without creating an issue first.

Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of
the pull request.

Description

Please include a summary of the change and which issue is fixed. Please also include relevant
motivation and context. List any dependencies that are required for this change.

Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as
    expected)
  • This change requires a documentation update

Screenshots/Sandbox (if appropriate/relevant):

Adding links to sandbox or providing screenshots can help us understand more about this PR and take
action on it as appropriate

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can
reproduce. Please also list any relevant details for your test configuration

  • Test A
  • Test B

Test Environment:

  • OS:
  • @graphql-mesh/...:
  • NodeJS:

Checklist:

  • I have followed the
    CONTRIBUTING doc and the
    style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works, and I have added a
    changeset using yarn changeset that bumps the version
  • New and existing unit tests and linter rules pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Further comments

If this is a relatively large or complex change, kick off the discussion by explaining why you chose
the solution you did and what alternatives you considered, etc...

Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added OpenTelemetry tracing spans for Redis cache operations, including initialization, get, set, and delete, improving observability across these workflows. No breaking changes.
  • Chores

    • Added a new runtime dependency on @opentelemetry/api.

Walkthrough

Adds OpenTelemetry tracing to @graphql-mesh/cache-redis (init, get, set, delete) and introduces a runtime dependency on @opentelemetry/api@^1.9.0. Changes include two changeset files documenting a minor and a patch release.

Changes

Cohort / File(s) Summary
Changesets
/.changeset/@graphql-mesh_cache-redis-8830-dependencies.md, /.changeset/shiny-paths-flow.md
Add release metadata: a patch changeset for dependency and a minor changeset documenting OTEL tracing spans for init/get/set/delete.
Package manifest
/packages/cache/redis/package.json
Add runtime dependency @opentelemetry/api at ^1.9.0.
Redis cache implementation
/packages/cache/redis/src/index.ts
Import OpenTelemetry, add a tracer: Tracer field, and wrap initialization and cache operations (init, set, get, delete) in tracing spans; ensure spans end on success and error across cluster/sentinel/URL/host/mock paths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as Application
  participant Cache as RedisCache
  participant OTEL as OpenTelemetry Tracer
  participant Redis as Redis Server/Cluster

  rect rgba(200,230,255,0.25)
  note over Cache,OTEL: Initialization
  App->>Cache: new RedisCache(options)
  Cache->>OTEL: startActiveSpan("hive.cache.redis.init")
  Cache->>Redis: connect (url/cluster/sentinel/mock)
  Redis-->>Cache: ready/connected or error
  Cache->>OTEL: end span
  end

  rect rgba(220,255,220,0.15)
  note over App,Redis: Get
  App->>Cache: get(key)
  Cache->>OTEL: startActiveSpan("hive.cache.get")
  Cache->>Redis: GET key
  Redis-->>Cache: value/null or error
  Cache->>OTEL: end span
  Cache-->>App: value/null or error
  end

  rect rgba(255,240,200,0.15)
  note over App,Redis: Set
  App->>Cache: set(key, value, ttl)
  Cache->>OTEL: startActiveSpan("hive.cache.set")
  Cache->>Redis: SET key value EX ttl
  Redis-->>Cache: OK or error
  Cache->>OTEL: end span
  Cache-->>App: result or error
  end

  rect rgba(255,220,220,0.15)
  note over App,Redis: Delete
  App->>Cache: delete(key)
  Cache->>OTEL: startActiveSpan("hive.cache.delete")
  Cache->>Redis: DEL key
  Redis-->>Cache: count or error
  Cache->>OTEL: end span
  Cache-->>App: count or error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ardatan
  • enisdenjo

Poem

I hop through keys where spans now gleam,
Tracing footsteps in a caching stream.
Init, get, set, delete — each little clue,
Carrots mark traces the devs can view. 🥕
A rabbit hums: observability anew.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The description is just the repository’s generic PR template with placeholders and does not provide any actual information about the specific changes, making it too vague to verify context or objectives. Please replace the template placeholders with a summary of the actual changes, link to the relevant issue, motivation and context, details on how the feature was tested, and any environment specifics.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately and concisely conveys the primary change by indicating that OpenTelemetry tracing has been added to the cache-redis component, matching the implemented feature in the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cache-redis/add-otel-traces

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/cache/redis/src/index.ts (1)

165-167: Consider adding tracing to getKeysByPrefix.

The getKeysByPrefix method doesn't have OpenTelemetry tracing while other cache operations (get, set, delete) do. Adding a span here would provide more complete observability coverage.

You could add tracing similar to the other methods:

 getKeysByPrefix(prefix: string): Promise<string[]> {
-  return scanPatterns(this.client, `${prefix}*`);
+  return this.tracer.startActiveSpan('hive.cache.getKeysByPrefix', span =>
+    scanPatterns(this.client, `${prefix}*`).finally(() => span.end()),
+  );
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da17e5b and 42ebc60.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • .changeset/@graphql-mesh_cache-redis-8830-dependencies.md (1 hunks)
  • .changeset/shiny-paths-flow.md (1 hunks)
  • packages/cache/redis/package.json (1 hunks)
  • packages/cache/redis/src/index.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/cache/redis/src/index.ts (3)
packages/legacy/types/src/index.ts (1)
  • Logger (202-211)
packages/legacy/types/src/config.ts (1)
  • Cache (1808-1817)
packages/legacy/types/src/pubsub.ts (3)
  • MeshPubSub (14-24)
  • HivePubSub (6-6)
  • toMeshPubSub (164-175)
🔇 Additional comments (7)
.changeset/shiny-paths-flow.md (1)

1-5: LGTM!

The changeset correctly documents the minor version bump and the new OTEL tracing feature for initialization and cache operations.

packages/cache/redis/src/index.ts (4)

14-14: LGTM!

The OpenTelemetry imports are correct and align with the newly added dependency.


23-23: LGTM!

The tracer field declaration is appropriate for storing the OpenTelemetry tracer instance.


144-155: LGTM!

The set method correctly wraps the Redis operation in a span and ensures proper span termination using finally blocks in both the TTL and non-TTL paths.


169-179: LGTM!

The delete method correctly wraps the Redis operation in a span and ensures the span is ended using a finally block.

.changeset/@graphql-mesh_cache-redis-8830-dependencies.md (1)

1-5: LGTM!

The changeset correctly documents the patch version bump and the addition of the @opentelemetry/api dependency.

packages/cache/redis/package.json (1)

41-41: Verified: @opentelemetry/api@^1.9.0 is current and secure
Latest release is 1.9.0 and no known vulnerabilities were found.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42ebc60 and b13a699.

📒 Files selected for processing (1)
  • packages/cache/redis/src/index.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/cache/redis/src/index.ts (3)
packages/legacy/types/src/index.ts (1)
  • Logger (202-211)
packages/legacy/types/src/config.ts (1)
  • Cache (1808-1817)
packages/legacy/types/src/pubsub.ts (3)
  • MeshPubSub (14-24)
  • HivePubSub (6-6)
  • toMeshPubSub (164-175)
🔇 Additional comments (4)
packages/cache/redis/src/index.ts (4)

14-14: LGTM!

The OpenTelemetry imports and tracer field are correctly added.

Also applies to: 23-23


28-140: LGTM! Past review comment addressed.

The try-finally pattern ensures the span ends even if initialization fails, addressing the previous concern about span leaks.


160-167: LGTM! Past review comment addressed.

The promise chain is now correct, with .finally(() => span.end()) properly chained to the promise. The span will end in all code paths.


173-182: LGTM!

The delete method correctly traces the operation with the span ending in all code paths via .finally().

@EmrysMyrddin EmrysMyrddin force-pushed the feat/cache-redis/add-otel-traces branch from 40e32e9 to 4eeae94 Compare October 10, 2025 09:28
Copy link
Contributor

Apollo Federation Subgraph Compatibility Results

Federation 1 Support Federation 2 Support
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢

Learn more:

Copy link
Contributor

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-mesh/cache-redis 0.105.0-alpha-20251010093057-4eeae9412ec39cfbab6e001ecaf527cf94f4ea31 npm ↗︎ unpkg ↗︎
@graphql-mesh/apollo-link 0.106.14-alpha-20251010093057-4eeae9412ec39cfbab6e001ecaf527cf94f4ea31 npm ↗︎ unpkg ↗︎

Copy link
Contributor

💻 Website Preview

The latest changes are available as preview in: https://b1d9fdf3.graphql-mesh.pages.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
packages/cache/redis/package.json (1)

41-41: Re-evaluate dependency type for @opentelemetry/api

To avoid duplicate API singletons across the monorepo/apps, consider making @opentelemetry/api a peerDependency (and keep as a regular dependency only if you explicitly want to bundle it here). Many OTEL libraries use a peer dependency on ^1.0.0 to share the global API.

packages/cache/redis/src/index.ts (5)

85-91: Prefer passing ioredis options programmatically over URL query params

ioredis does not support enableAutoPipelining/enableOfflineQueue/lazyConnect via connection string query params. Pass them via the options argument to new Redis(url, options) to ensure they take effect.

-          if (lazyConnect) {
-            redisUrl.searchParams.set('lazyConnect', 'true');
-          }
-
-          redisUrl.searchParams.set('enableAutoPipelining', 'true');
-          redisUrl.searchParams.set('enableOfflineQueue', 'true');
-          const IPV6_REGEX =
+          const IPV6_REGEX =
             /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-5]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/gm;
-          if (IPV6_REGEX.test(redisUrl.hostname)) {
-            redisUrl.searchParams.set('family', '6');
-          }
-          const urlStr = redisUrl.toString();
-          options.logger.debug(`Connecting to Redis at ${urlStr}`);
-          this.client = new Redis(urlStr);
+          const family = IPV6_REGEX.test(redisUrl.hostname) ? 6 : undefined;
+          const urlStr = redisUrl.toString();
+          const logUrl = new URL(urlStr);
+          if (logUrl.password) {
+            logUrl.password = '***';
+          }
+          options.logger.debug(`Connecting to Redis at ${logUrl.toString()}`);
+          this.client = new Redis(urlStr, {
+            enableAutoPipelining: true,
+            enableOfflineQueue: true,
+            ...(lazyConnect ? { lazyConnect: true } : {}),
+            ...(family ? { family } : {}),
+          });

Also applies to: 96-99


38-44: Add radix to parseInt for clarity and correctness

Explicitly pass 10 to parseInt to avoid edge cases and improve readability.

-          const numDb = parseInt(parsedDb);
+          const numDb = parseInt(parsedDb, 10);
 ...
-              port: s.port && parseInt(interpolateStrWithEnv(s.port)),
-              family: s.family && parseInt(interpolateStrWithEnv(s.family)),
+              port: s.port && parseInt(interpolateStrWithEnv(s.port), 10),
+              family: s.family && parseInt(interpolateStrWithEnv(s.family), 10),
 ...
-              port: s.port && parseInt(interpolateStrWithEnv(s.port)),
-              family: s.family && parseInt(interpolateStrWithEnv(s.family)),
+              port: s.port && parseInt(interpolateStrWithEnv(s.port), 10),
+              family: s.family && parseInt(interpolateStrWithEnv(s.family), 10),
 ...
-          const numPort = parseInt(parsedPort);
-          const numDb = parseInt(parsedDb);
+          const numPort = parseInt(parsedPort, 10);
+          const numDb = parseInt(parsedDb, 10);

Also applies to: 69-71, 112-113


131-136: Guard unsubscribe call against undefined values

id and pubsub can be undefined at type-level. Guard to satisfy strict TS and avoid accidental runtime misuse.

-        const id = pubsub?.subscribe('destroy', () => {
-          this.client.disconnect(false);
-          pubsub.unsubscribe(id);
-        });
+        const id = pubsub?.subscribe('destroy', () => {
+          this.client.disconnect(false);
+          if (pubsub && id != null) {
+            pubsub.unsubscribe(id);
+          }
+        });

14-14: Record exceptions and set span status on errors

Improve observability by recording exceptions and setting SpanStatusCode.ERROR when operations fail.

-import { trace, type Tracer } from '@opentelemetry/api';
+import { trace, type Tracer, SpanStatusCode } from '@opentelemetry/api';
@@
   set(key: string, value: V, options?: KeyValueCacheSetOptions): Promise<any> {
     return this.tracer.startActiveSpan('hive.cache.set', async span => {
-      try {
+      try {
         const stringifiedValue = JSON.stringify(value);
         if (options?.ttl && options.ttl > 0) {
           return await this.client.set(key, stringifiedValue, 'PX', options.ttl * 1000);
         } else {
           return await this.client.set(key, stringifiedValue);
         }
-      } finally {
+      } catch (err) {
+        span.recordException(err as Error);
+        span.setStatus({ code: SpanStatusCode.ERROR });
+        throw err;
+      } finally {
         span.end();
       }
     });
   }
@@
   get(key: string): Promise<V | undefined> {
     return this.tracer.startActiveSpan('hive.cache.get', span =>
       this.client
         .get(key)
         .then(value => (value != null ? JSON.parse(value) : undefined))
+        .catch(err => {
+          span.recordException(err as Error);
+          span.setStatus({ code: SpanStatusCode.ERROR });
+          throw err;
+        })
         .finally(() => span.end()),
     );
   }
@@
   delete(key: string): Promise<boolean> {
     return this.tracer.startActiveSpan('hive.cache.delete', span =>
       this.client
         .del(key)
         .then(
           value => value > 0,
           () => false,
         )
+        .catch(err => {
+          span.recordException(err as Error);
+          span.setStatus({ code: SpanStatusCode.ERROR });
+          throw err;
+        })
         .finally(() => span.end()),
     );
   }

Also applies to: 147-159, 162-169, 175-184


28-31: Optional: align tracer/span naming with package

Consider using '@graphql-mesh/cache-redis' as tracer name and spans like 'cache.redis.init|get|set|delete' for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b13a699 and 4eeae94.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • .changeset/@graphql-mesh_cache-redis-8830-dependencies.md (1 hunks)
  • .changeset/shiny-paths-flow.md (1 hunks)
  • packages/cache/redis/package.json (1 hunks)
  • packages/cache/redis/src/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/shiny-paths-flow.md
🧰 Additional context used
🧬 Code graph analysis (1)
packages/cache/redis/src/index.ts (2)
packages/legacy/types/src/index.ts (1)
  • Logger (202-211)
packages/legacy/types/src/pubsub.ts (3)
  • MeshPubSub (14-24)
  • HivePubSub (6-6)
  • toMeshPubSub (164-175)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: release / snapshot
  • GitHub Check: e2e / node v22
  • GitHub Check: integration / node 20
  • GitHub Check: unit / node 22
  • GitHub Check: integration / node 22
  • GitHub Check: apollo-federation-compatibility
  • GitHub Check: e2e / node v18
  • GitHub Check: integration / node 18
  • GitHub Check: unit / node 20
  • GitHub Check: check
  • GitHub Check: e2e / node v20
  • GitHub Check: unit / node 18
  • GitHub Check: deployment
🔇 Additional comments (1)
.changeset/@graphql-mesh_cache-redis-8830-dependencies.md (1)

1-5: Changeset looks good; confirm feature changeset exists

Patch for dependency addition is fine. If OTEL tracing is a user-visible feature, ensure there’s a separate changeset entry (likely minor) describing the tracing spans for init/get/set/delete so release notes are accurate.

Comment on lines +96 to +99
const urlStr = redisUrl.toString();
options.logger.debug(`Connecting to Redis at ${urlStr}`);
this.client = new Redis(urlStr);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not log Redis credentials (secret leakage)

Logging urlStr may include username/password from options.url. Sanitize before logging.

Apply this diff:

-          const urlStr = redisUrl.toString();
-          options.logger.debug(`Connecting to Redis at ${urlStr}`);
-          this.client = new Redis(urlStr);
+          const urlStr = redisUrl.toString();
+          // Sanitize credentials for logs
+          const logUrl = new URL(urlStr);
+          if (logUrl.password) {
+            logUrl.password = '***';
+          }
+          options.logger.debug(`Connecting to Redis at ${logUrl.toString()}`);
+          this.client = new Redis(urlStr);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const urlStr = redisUrl.toString();
options.logger.debug(`Connecting to Redis at ${urlStr}`);
this.client = new Redis(urlStr);
} else {
const urlStr = redisUrl.toString();
// Sanitize credentials for logs
const logUrl = new URL(urlStr);
if (logUrl.password) {
logUrl.password = '***';
}
options.logger.debug(`Connecting to Redis at ${logUrl.toString()}`);
this.client = new Redis(urlStr);
} else {
🤖 Prompt for AI Agents
packages/cache/redis/src/index.ts lines 96-99: the code logs the full redis URL
(urlStr) which can contain username/password and leak secrets; instead, before
logging build a sanitized string that strips or redacts credentials (e.g., parse
the URL and omit username/password, or replace them with ****, or log only host
and port), use that sanitized value in options.logger.debug while still using
the original urlStr when creating this.client = new Redis(urlStr).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant