2828import com .google .protobuf .DynamicMessage ;
2929import com .google .protobuf .InvalidProtocolBufferException ;
3030import com .google .protobuf .Message ;
31+ import io .opentelemetry .api .trace .Span ;
32+ import io .opentelemetry .api .trace .Tracer ;
33+ import io .opentelemetry .api .trace .propagation .W3CTraceContextPropagator ;
34+ import io .opentelemetry .context .Context ;
35+ import io .opentelemetry .context .Scope ;
36+ import io .opentelemetry .context .propagation .TextMapGetter ;
37+ import io .opentelemetry .context .propagation .TextMapSetter ;
3138import java .io .IOException ;
3239import java .io .Serializable ;
3340import java .nio .charset .StandardCharsets ;
41+ import java .util .HashMap ;
3442import java .util .List ;
3543import java .util .Map ;
3644import java .util .Objects ;
5260import org .apache .beam .sdk .io .gcp .pubsub .PubsubClient .TopicPath ;
5361import org .apache .beam .sdk .metrics .Lineage ;
5462import org .apache .beam .sdk .options .PipelineOptions ;
63+ import org .apache .beam .sdk .options .SdkHarnessOptions ;
5564import org .apache .beam .sdk .options .ValueProvider ;
5665import org .apache .beam .sdk .options .ValueProvider .NestedValueProvider ;
5766import org .apache .beam .sdk .options .ValueProvider .StaticValueProvider ;
9099import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .ImmutableMap ;
91100import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .Lists ;
92101import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .Maps ;
102+ import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
93103import org .checkerframework .checker .nullness .qual .Nullable ;
94104import org .joda .time .Instant ;
95105import org .slf4j .Logger ;
@@ -880,6 +890,8 @@ public abstract static class Read<T> extends PTransform<PBegin, PCollection<T>>
880890
881891 abstract boolean getNeedsMessageId ();
882892
893+ abstract boolean isEnableOpenTelemetryTracing ();
894+
883895 abstract boolean getNeedsOrderingKey ();
884896
885897 abstract BadRecordRouter getBadRecordRouter ();
@@ -900,6 +912,7 @@ static <T> Builder<T> newBuilder(SerializableFunction<PubsubMessage, T> parseFn)
900912 builder .setBadRecordRouter (BadRecordRouter .THROWING_ROUTER );
901913 builder .setBadRecordErrorHandler (new DefaultErrorHandler <>());
902914 builder .setValidate (false );
915+ builder .setEnableOpenTelemetryTracing (false );
903916 return builder ;
904917 }
905918
@@ -922,6 +935,8 @@ abstract static class Builder<T> {
922935
923936 abstract Builder <T > setIdAttribute (String idAttribute );
924937
938+ abstract Builder <T > setEnableOpenTelemetryTracing (boolean enableOpenTelemetryTracing );
939+
925940 abstract Builder <T > setCoder (Coder <T > coder );
926941
927942 abstract Builder <T > setParseFn (SerializableFunction <PubsubMessage , T > parseFn );
@@ -1105,6 +1120,66 @@ public Read<T> withIdAttribute(String idAttribute) {
11051120 return toBuilder ().setIdAttribute (idAttribute ).build ();
11061121 }
11071122
1123+ public Read <T > withEnableOpenTelemetryTracing () {
1124+ return toBuilder ().setEnableOpenTelemetryTracing (true ).setNeedsAttributes (true ).build ();
1125+ }
1126+
1127+ static class OpenTelemetryHeaderConsumer extends DoFn <PubsubMessage , PubsubMessage > {
1128+
1129+ Context extractSpanContext (PubsubMessage message ) {
1130+ TextMapGetter <PubsubMessage > extractMessageAttributes =
1131+ new TextMapGetter <PubsubMessage >() {
1132+ @ Override
1133+ public @ Nullable String get (@ Nullable PubsubMessage carrier , String key ) {
1134+ if (carrier == null ) {
1135+ return null ;
1136+ }
1137+ return carrier .getAttribute ("googclient_" + key );
1138+ }
1139+
1140+ @ Override
1141+ public Iterable <String > keys (PubsubMessage carrier ) {
1142+ Map <String , String > attributeMap = carrier .getAttributeMap ();
1143+ if (attributeMap == null ) {
1144+ return ImmutableList .of ();
1145+ }
1146+ List <String > keys = new java .util .ArrayList <>();
1147+ for (String key : attributeMap .keySet ()) {
1148+ if (key .startsWith ("googclient_" )) {
1149+ keys .add (key .substring ("googclient_" .length ()));
1150+ }
1151+ }
1152+ return keys ;
1153+ }
1154+ };
1155+ return W3CTraceContextPropagator .getInstance ()
1156+ .extract (Context .current (), message , extractMessageAttributes );
1157+ }
1158+
1159+ @ Setup
1160+ public void setup (PipelineOptions po ) {
1161+ tracer = po .as (SdkHarnessOptions .class ).getOpenTelemetry ().getTracer ("PubSubIO" );
1162+ }
1163+
1164+ private transient @ MonotonicNonNull Tracer tracer = null ;
1165+
1166+ @ ProcessElement
1167+ public void processElement (
1168+ @ Element PubsubMessage message , OutputReceiver <PubsubMessage > output ) {
1169+ @ Nullable Context context = extractSpanContext (message );
1170+ Span span =
1171+ checkArgumentNotNull (tracer )
1172+ .spanBuilder ("PubSubIO.Read" )
1173+ .setParent (context )
1174+ .startSpan ();
1175+ try (Scope s = span .makeCurrent ()) {
1176+ output .output (message );
1177+ } finally {
1178+ span .end ();
1179+ }
1180+ }
1181+ }
1182+
11081183 /**
11091184 * Causes the source to return a PubsubMessage that includes Pubsub attributes, and uses the
11101185 * given parsing function to transform the PubsubMessage into an output type. A Coder for the
@@ -1234,6 +1309,12 @@ public T apply(PubsubMessage input) {
12341309 };
12351310 ValueProvider <PubsubTopic > deadLetterTopicProvider = getDeadLetterTopicProvider ();
12361311 PCollection <T > read ;
1312+ if (isEnableOpenTelemetryTracing ()) {
1313+ preParse =
1314+ preParse .apply (
1315+ "Extract OpenTelemetry context from Header" ,
1316+ ParDo .of (new OpenTelemetryHeaderConsumer ()));
1317+ }
12371318 if (deadLetterTopicProvider == null
12381319 && (getBadRecordRouter () instanceof ThrowingBadRecordRouter )) {
12391320 read = preParse .apply (MapElements .into (typeDescriptor ).via (parseFnWrapped ));
@@ -1413,6 +1494,8 @@ public abstract static class Write<T> extends PTransform<PCollection<T>, PDone>
14131494
14141495 abstract @ Nullable String getPubsubRootUrl ();
14151496
1497+ abstract boolean isEnableOpenTelemetryTracing ();
1498+
14161499 abstract boolean getPublishWithOrderingKey ();
14171500
14181501 abstract BadRecordRouter getBadRecordRouter ();
@@ -1432,6 +1515,7 @@ static <T> Builder<T> newBuilder(
14321515 builder .setBadRecordErrorHandler (new DefaultErrorHandler <>());
14331516 builder .setPublishWithOrderingKey (false );
14341517 builder .setValidate (false );
1518+ builder .setEnableOpenTelemetryTracing (false );
14351519 return builder ;
14361520 }
14371521
@@ -1456,6 +1540,8 @@ abstract Builder<T> setTopicFunction(
14561540
14571541 abstract Builder <T > setTimestampAttribute (String timestampAttribute );
14581542
1543+ abstract Builder <T > setEnableOpenTelemetryTracing (boolean enableOpenTelemetryTracing );
1544+
14591545 abstract Builder <T > setIdAttribute (String idAttribute );
14601546
14611547 abstract Builder <T > setFormatFn (
@@ -1475,6 +1561,41 @@ abstract Builder<T> setBadRecordErrorHandler(
14751561 abstract Write <T > build ();
14761562 }
14771563
1564+ static class OpenTelemetryHeaderPropagator extends DoFn <PubsubMessage , PubsubMessage > {
1565+ void injectSpanContext (Map <String , String > attr ) {
1566+ TextMapSetter <Map <String , String >> inject =
1567+ new TextMapSetter <Map <String , String >>() {
1568+ @ Override
1569+ public void set (@ Nullable Map <String , String > attr , String key , String value ) {
1570+ if (attr != null ) {
1571+ attr .put ("googclient_" + key , value );
1572+ }
1573+ }
1574+ };
1575+ W3CTraceContextPropagator .getInstance ().inject (Context .current (), attr , inject );
1576+ }
1577+
1578+ @ ProcessElement
1579+ public void processElement (
1580+ @ Element PubsubMessage message , OutputReceiver <PubsubMessage > output ) {
1581+ Map <String , String > attributeMap = message .getAttributeMap ();
1582+ Map <String , String > attr =
1583+ attributeMap == null ? new HashMap <>() : new HashMap <>(attributeMap );
1584+ injectSpanContext (attr );
1585+
1586+ // copy the message, multiple fields
1587+ PubsubMessage ps =
1588+ new PubsubMessage (
1589+ message .getPayload (), attr , message .getMessageId (), message .getOrderingKey ());
1590+
1591+ // topic is copied seperately, not via constructor
1592+ if (message .getTopic () != null ) {
1593+ ps = ps .withTopic (message .getTopic ());
1594+ }
1595+ output .output (ps );
1596+ }
1597+ }
1598+
14781599 /**
14791600 * Publishes to the specified topic.
14801601 *
@@ -1550,6 +1671,10 @@ public Write<T> withMaxBatchBytesSize(int maxBatchBytesSize) {
15501671 return toBuilder ().setMaxBatchBytesSize (maxBatchBytesSize ).build ();
15511672 }
15521673
1674+ public Write <T > withEnableOpenTelemetryTracing () {
1675+ return toBuilder ().setEnableOpenTelemetryTracing (true ).build ();
1676+ }
1677+
15531678 /**
15541679 * Writes to Pub/Sub with each record's ordering key. A subscription with message ordering
15551680 * enabled will receive messages published in the same region with the same ordering key in the
@@ -1657,6 +1782,11 @@ public PDone expand(PCollection<T> input) {
16571782 } else {
16581783 pubsubMessages .setCoder (PubsubMessageWithTopicCoder .of ());
16591784 }
1785+ if (isEnableOpenTelemetryTracing ()) {
1786+ pubsubMessages =
1787+ pubsubMessages .apply (
1788+ "Propagate OpenTelemetry Tracing" , ParDo .of (new OpenTelemetryHeaderPropagator ()));
1789+ }
16601790 switch (input .isBounded ()) {
16611791 case BOUNDED :
16621792 pubsubMessages .apply (
0 commit comments