Repository: googleapis/nodejs-pubsub

Issue:
Update native OTel to semantic conventions 1.24.0
### A screenshot that you have tested with "Try this API".


N/A.

### What would you like to see in the library?


In order to be [spec compliant](https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/messaging/messaging-spans.md) with the latest stable version of OpenTelemetry semantic conventions, the `@google-cloud/pubsub` library needs to add the following attribute to its spans:  `messaging.operation` (required). `messaging.operation` is already included in the span name; it just needs to added to the span attributes as well.

This is a S change that I can submit a PR for; we just need to add these attributes to the `create...Span` methods in `src/telemetry-tracing.ts`:
* For publisher spans, `messaging.operation` will be set to `create`. 
* For subscriber spans, `messaging.operation` will be set to `receive`.

### Describe alternatives you've considered

_No response_

### Additional context/notes

_No response_

Discussion:
@bizob2828: Updating the telemetry tracing to be compliant with the "stable" 1.24.0 semantic conventions also requires updates to span kind for ack and modack. Currently they are being assigned `consumer` span kind but based on those conventions they should be `client` span.kind or at the very least `internal`. Consumer implies special rules in opentelemetry indicating it's the entry point into an application and special logic must be applied

Solution patch:
diff --git a/src/telemetry-tracing.ts b/src/telemetry-tracing.ts
index 459ea7b..e6f44b0 100644
--- a/src/telemetry-tracing.ts
+++ b/src/telemetry-tracing.ts
@@ -305,6 +305,7 @@ export class PubsubSpans {
     params: AttributeParams,
     message?: PubsubMessage,
     caller?: string,
+    operation?: string,
   ): SpanAttributes {
     const destinationName = params.topicName ?? params.subName;
     const destinationId = params.topicId ?? params.subId;
@@ -351,6 +352,9 @@ export class PubsubSpans {
       if (message.ackId) {
         spanAttributes['messaging.gcp_pubsub.message.ack_id'] = message.ackId;
       }
+      if (operation) {
+        spanAttributes['messaging.operation'] = operation;
+      }
     }
 
     return spanAttributes;
@@ -368,11 +372,15 @@ export class PubsubSpans {
     const topicInfo = getTopicInfo(topicName);
     const span: Span = getTracer().startSpan(`${topicName} create`, {
       kind: SpanKind.PRODUCER,
-      attributes: PubsubSpans.createAttributes(topicInfo, message, caller),
+      attributes: PubsubSpans.createAttributes(
+        topicInfo,
+        message,
+        caller,
+        'create',
+      ),
     });
     if (topicInfo.topicId) {
       span.updateName(`${topicInfo.topicId} create`);
-      span.setAttribute('messaging.operation', 'create');
       span.setAttribute('messaging.destination.name', topicInfo.topicId);
     }
 
@@ -404,10 +412,14 @@ export class PubsubSpans {
 
     const subInfo = getSubscriptionInfo(subName);
     const name = `${subInfo.subId ?? subName} subscribe`;
-    const attributes = this.createAttributes(subInfo, message, caller);
+    const attributes = this.createAttributes(
+      subInfo,
+      message,
+      caller,
+      'receive',
+    );
     if (subInfo.subId) {
       attributes['messaging.destination.name'] = subInfo.subId;
-      attributes['messaging.operation'] = 'receive';
     }
 
     if (context) {
@@ -473,6 +485,7 @@ export class PubsubSpans {
       getTopicInfo(topicName),
       undefined,
       caller,
+      'create',
     );
     const links: Link[] = messages
       .filter(m => m.parentSpan && isSampled(m.parentSpan))
@@ -515,6 +528,7 @@ export class PubsubSpans {
       subInfo,
       undefined,
       caller,
+      'receive',
     );
     const links: Link[] = messageSpans
       .filter(m => m && isSampled(m))
@@ -531,7 +545,6 @@ export class PubsubSpans {
     );
 
     span?.setAttribute('messaging.batch.message_count', messageSpans.length);
-    span?.setAttribute('messaging.operation', 'receive');
 
     if (span) {
       // Also attempt to link from the subscribe span(s) back to the publish RPC span.
@@ -563,6 +576,7 @@ export class PubsubSpans {
       subInfo,
       undefined,
       caller,
+      'receive',
     );
     const links: Link[] = messageSpans
       .filter(m => m && isSampled(m))
@@ -579,7 +593,6 @@ export class PubsubSpans {
     );
 
     span?.setAttribute('messaging.batch.message_count', messageSpans.length);
-    span?.setAttribute('messaging.operation', 'receive');
 
     if (span) {
       // Also attempt to link from the subscribe span(s) back to the publish RPC span.



Test patch:
diff --git a/test/telemetry-tracing.ts b/test/telemetry-tracing.ts
index 2bff66e..02715a3 100644
--- a/test/telemetry-tracing.ts
+++ b/test/telemetry-tracing.ts
@@ -212,6 +212,7 @@ describe('OpenTelemetryTracer', () => {
         topicInfo,
         message,
         'tests',
+        'create',
       );
       assert.deepStrictEqual(topicAttrs, {
         'messaging.system': 'gcp_pubsub',
@@ -222,6 +223,7 @@ describe('OpenTelemetryTracer', () => {
         'messaging.gcp_pubsub.message.exactly_once_delivery':
           message.isExactlyOnceDelivery,
         'messaging.gcp_pubsub.message.ack_id': message.ackId,
+        'messaging.operation': 'create',
         'code.function': 'tests',
       });
 
@@ -235,10 +237,12 @@ describe('OpenTelemetryTracer', () => {
         topicInfo,
         message,
         'tests',
+        'create',
       );
       assert.deepStrictEqual(topicAttrs2, {
         'messaging.system': 'gcp_pubsub',
         'messaging.destination.name': topicInfo.topicId,
+        'messaging.operation': 'create',
         'gcp.project_id': topicInfo.projectId,
         'messaging.message.envelope.size': message.data?.length,
         'code.function': 'tests',
@@ -283,6 +287,7 @@ describe('OpenTelemetryTracer', () => {
       const firstSpan = spans.pop();
       assert.ok(firstSpan);
       assert.strictEqual(firstSpan.name, `${tests.topicInfo.topicId} create`);
+      assert.strictEqual(firstSpan.attributes['messaging.operation'], 'create');
       assert.strictEqual(
         firstSpan.attributes['messaging.destination.name'],
         tests.topicInfo.topicId,
@@ -312,7 +317,6 @@ describe('OpenTelemetryTracer', () => {
       const firstSpan = spans.pop();
       assert.ok(firstSpan);
       assert.strictEqual(firstSpan.name, 'other create');
-
       assert.strictEqual(
         firstSpan.attributes['messaging.destination.name'],
         'other',
@@ -346,6 +350,10 @@ describe('OpenTelemetryTracer', () => {
         childReadSpan.attributes['messaging.destination.name'],
         'sub',
       );
+      assert.strictEqual(
+        childReadSpan.attributes['messaging.operation'],
+        'receive',
+      );
       assert.strictEqual(childReadSpan.kind, SpanKind.CONSUMER);
       assert.ok(childReadSpan.parentSpanId);
     });