rzo1 commented on code in PR #9154:
URL: https://github.com/apache/storm/pull/9154#discussion_r4269207001


##########
storm-client/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java:
##########
@@ -64,4 +81,18 @@ public byte[] serialize(Tuple tuple) {
             throw new RuntimeException(e);
         }
     }
+
+    /**
+     * Appends an entry after the values: a tag byte, the payload length as a 
varint, then the payload. Readers skip entries with
+     * an unknown tag, and readers that stop after the values ignore all of 
them.
+     */
+    private void writeTraceContext(Object traceContext) {
+        byte[] payload = tracer.encode(traceContext);

Review Comment:
   Minor: with an all grouping to tasks on remote workers, the same context is 
encoded once per destination, including the construction of the tracestate 
string if one is present. Caching the encoded form per context object would 
avoid this, although the gain is presumably small.



##########
external/storm-opentelemetry/src/main/java/org/apache/storm/opentelemetry/OpenTelemetryTupleTracer.java:
##########
@@ -0,0 +1,290 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The 
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with 
the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.opentelemetry;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import java.net.UnknownHostException;
+import java.time.Instant;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.storm.Config;
+import org.apache.storm.task.WorkerTopologyContext;
+import org.apache.storm.tracing.TupleTracer;
+import org.apache.storm.tuple.Tuple;
+import org.apache.storm.utils.Time;
+import org.apache.storm.utils.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Traces tuple trees with OpenTelemetry. To use it, set {@link 
Config#TOPOLOGY_TRACING_TRACER} to this class name. Spans go to
+ * the SDK registered as the global OpenTelemetry instance, usually by the 
OpenTelemetry Java agent, version 2.23.0 or later. Until
+ * one is registered, tuples are not traced and one warning is logged.
+ *
+ * <p>Each spout emit starts a trace with a root span named "component emit", 
started and ended at once. Each bolt
+ * {@code execute()} of a traced tuple runs in a child span named "component 
execute", current while {@code execute()} runs. A bolt
+ * emit carries the context of its anchors: the anchor's context if they share 
one span. Otherwise it carries the context of a span
+ * named "component emit", started and ended at once: a child of the first 
anchor's span linked to the others if all anchors belong
+ * to one trace, else a new root span linked to each.
+ *
+ * <p>The spout records the ack, fail or timeout of each traced tuple tree as 
a span whose duration, also set as the
+ * {@code storm.tuple.latency_ms} attribute, is the time from the emit to the 
outcome. A bolt records each {@code fail()} as a span
+ * started and ended at once. Contexts with the sampled flag unset are 
propagated too.
+ */
+public class OpenTelemetryTupleTracer implements TupleTracer {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenTelemetryTupleTracer.class);
+    private static final String INSTRUMENTATION_SCOPE = "org.apache.storm";
+    private static final long SDK_LOOKUP_INTERVAL_MS = 1000;
+    private static final long UNTRACED_EMIT_LOG_INTERVAL_MS = 60_000;
+
+    private static final AttributeKey<String> TOPOLOGY_NAME_KEY = 
AttributeKey.stringKey("storm.topology.name");
+    private static final AttributeKey<String> TOPOLOGY_ID_KEY = 
AttributeKey.stringKey("storm.topology.id");
+    private static final AttributeKey<String> COMPONENT_ID_KEY = 
AttributeKey.stringKey("storm.component.id");
+    private static final AttributeKey<Long> TASK_ID_KEY = 
AttributeKey.longKey("storm.task.id");
+    private static final AttributeKey<String> SOURCE_COMPONENT_ID_KEY = 
AttributeKey.stringKey("storm.source.component.id");
+    private static final AttributeKey<String> SOURCE_STREAM_ID_KEY = 
AttributeKey.stringKey("storm.source.stream.id");
+    private static final AttributeKey<String> WORKER_HOST_KEY = 
AttributeKey.stringKey("storm.worker.host");
+    private static final AttributeKey<Long> WORKER_PORT_KEY = 
AttributeKey.longKey("storm.worker.port");
+    private static final AttributeKey<Long> TUPLE_LATENCY_KEY = 
AttributeKey.longKey("storm.tuple.latency_ms");
+
+    private WorkerTopologyContext context;
+    private Attributes workerAttributes;
+    /** Indexed by task id; null for the tasks of other workers and for system 
tasks with a negative id. */
+    private TaskSpans[] taskSpans;
+    private volatile Tracer tracer;
+    private volatile long nextSdkLookupMs;
+    private final AtomicBoolean warnedNoSdk = new AtomicBoolean();
+
+    @Override
+    public void prepare(Map<String, Object> topoConf, WorkerTopologyContext 
context) {
+        this.context = context;
+        AttributesBuilder attributes = Attributes.builder()
+            .put(TOPOLOGY_NAME_KEY, (String) 
topoConf.get(Config.TOPOLOGY_NAME))
+            .put(TOPOLOGY_ID_KEY, context.getStormId())
+            .put(WORKER_PORT_KEY, context.getThisWorkerPort().longValue());
+        try {
+            attributes.put(WORKER_HOST_KEY, Utils.hostname());
+        } catch (UnknownHostException e) {
+            LOG.warn("Execute spans get no {} attribute: the host name is 
unknown", WORKER_HOST_KEY, e);
+        }
+        this.workerAttributes = attributes.build();
+        int maxTaskId = 
context.getThisWorkerTasks().stream().mapToInt(Integer::intValue).max().orElse(-1);
+        TaskSpans[] byTask = new TaskSpans[maxTaskId + 1];
+        for (int taskId : context.getThisWorkerTasks()) {
+            if (taskId >= 0) {
+                byTask[taskId] = newTaskSpans(taskId);
+            }
+        }
+        this.taskSpans = byTask;
+    }
+
+    @Override
+    public Object spoutEmit(int taskId, String streamId) {
+        Tracer current = tracer();
+        return current == null ? null : 
emitContext(current.spanBuilder(spans(taskId).emitName()).setNoParent());
+    }
+
+    @Override
+    public Object boltEmit(int taskId, String streamId, List<Object> 
anchorContexts) {
+        if (anchorContexts.isEmpty()) {
+            logUntracedEmitUnderSpan(taskId, streamId);
+            return null;
+        }
+        Context first = (Context) anchorContexts.get(0);
+        if (anchorContexts.size() == 1) {
+            return first;
+        }
+        SpanContext firstSpan = Span.fromContext(first).getSpanContext();
+        Set<SpanContext> otherSpans = new LinkedHashSet<>();

Review Comment:
   This set grows with the number of anchors, whereas at most 128 links are 
retained (line 135). Stopping once 129 distinct span contexts have been 
collected would bound the cost for emits with many anchors without changing the 
result.



##########
storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java:
##########
@@ -662,6 +674,17 @@ public final WorkerUserContext getWorkerUserContext() {
         }
     }
 
+    private TupleTracer mkTupleTracer() {
+        String className = (String) 
topologyConf.get(Config.TOPOLOGY_TRACING_TRACER);
+        if (className == null) {
+            return null;
+        }
+        TupleTracer tracer = ReflectionUtils.newInstance(className);

Review Comment:
   Minor: if the class is not on the worker classpath, for instance because 
`storm-opentelemetry` is missing from `lib-worker`, this call fails on every 
worker, and the workers are restarted repeatedly. A sentence in `Tracing.md` 
would make this failure mode easier to diagnose.



##########
storm-client/src/jvm/org/apache/storm/executor/bolt/BoltOutputCollectorImpl.java:
##########
@@ -117,6 +126,26 @@ private List<Integer> boltEmit(String streamId, 
Collection<Tuple> anchors, List<
         return outTasks;
     }
 
+    /**
+     * Returns the trace contexts of the anchors, in anchor order, skipping 
anchors without one.
+     */
+    private static List<Object> traceContexts(Collection<Tuple> anchors) {

Review Comment:
   For windowed bolts, every emit is anchored to the whole window, so this 
method allocates a list of the window's size on each emit, and `boltEmit` 
subsequently builds a set of all span contexts (see the corresponding comment 
in `OpenTelemetryTupleTracer`). Since the SDK retains at most 128 links by 
default, the work is largely discarded.



##########
storm-client/src/jvm/org/apache/storm/executor/bolt/BoltExecutor.java:
##########
@@ -224,7 +225,18 @@ public void tupleActionFn(int taskId, TupleImpl tuple) 
throws Exception {
             if (isExecuteSampler) {
                 tuple.setExecuteSampleStartTime(now);
             }
-            boltObject.execute(tuple);
+            Object received = tupleTracer == null ? null : 
tuple.getTraceContext();
+            TupleTracer.ExecuteScope scope =
+                received == null ? null : tupleTracer.startExecute(taskId, 
tuple, received);
+            if (scope == null) {
+                boltObject.execute(tuple);
+            } else {
+                try (scope) {
+                    // anchored emits and fail() read the context from the 
tuple, also after execute() returns
+                    tuple.setTraceContext(scope.context());
+                    boltObject.execute(tuple);
+                }
+            }
 
             Long ms = tuple.getExecuteSampleStartTime();

Review Comment:
   Since `delta` is computed after the `try (scope)` block, the execute latency 
now includes the start of the span, `span.end()` and the enqueueing into the 
batch span processor. This affects the execute latency and capacity shown in 
the UI, the task metrics and `BoltExecuteInfo`, so that a comparison with 
tracing enabled and disabled overstates the cost of the bolt itself. The time 
could be taken before the scope is closed; alternatively, the behaviour should 
be documented.



##########
storm-client/src/jvm/org/apache/storm/tracing/TupleTracer.java:
##########
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The 
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with 
the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.tracing;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.task.WorkerTopologyContext;
+import org.apache.storm.tuple.Tuple;
+
+/**
+ * Attaches a trace context to tuples and records how their tuple trees are 
processed. Each worker creates one instance from the
+ * class named in {@link Config#TOPOLOGY_TRACING_TRACER}, through its zero-arg 
constructor, and shares it between its executors
+ * and the threads that serialize and deserialize tuples, so implementations 
must be thread safe.
+ *
+ * <p>A context is any object the implementation chooses; null means not 
traced. Storm keeps it on the tuple and, for the tuple
+ * trees a spout starts, until the tree completes. Tuples sent to another 
worker carry the bytes {@link #encode} returns, and the
+ * receiving worker turns them back into a context with {@link #decode}.
+ *
+ * <p>An exception from {@link #decode} leaves the tuple without a context. 
Exceptions from the other methods propagate to the

Review Comment:
   According to this contract, exceptions from every method except `decode` 
propagate to the caller. An exception in `encode` therefore fails 
`KryoTupleSerializer.serialize()` and terminates the worker, and the same holds 
for `spoutEmit` and `boltEmit` (which run within the user's `emit()`), 
`startExecute`, and `spoutOutcome` (which runs before `spout.ack()`). The 
OpenTelemetry API is designed not to throw, so the bundled tracer is unlikely 
to be affected, but the contract applies to every implementation. Since the 
tracer serves observability only, I would expect a failure to leave the 
processing of tuples unaffected, as is already the case for `decode`. Catching 
`RuntimeException` around the calls in storm-client, logging at a limited rate 
and treating the result as null would achieve this without cost on the regular 
path. Was the propagation a deliberate choice?



##########
storm-client/src/jvm/org/apache/storm/tracing/TupleTracer.java:
##########
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The 
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with 
the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.tracing;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.task.WorkerTopologyContext;
+import org.apache.storm.tuple.Tuple;
+
+/**
+ * Attaches a trace context to tuples and records how their tuple trees are 
processed. Each worker creates one instance from the
+ * class named in {@link Config#TOPOLOGY_TRACING_TRACER}, through its zero-arg 
constructor, and shares it between its executors
+ * and the threads that serialize and deserialize tuples, so implementations 
must be thread safe.
+ *
+ * <p>A context is any object the implementation chooses; null means not 
traced. Storm keeps it on the tuple and, for the tuple
+ * trees a spout starts, until the tree completes. Tuples sent to another 
worker carry the bytes {@link #encode} returns, and the
+ * receiving worker turns them back into a context with {@link #decode}.
+ *
+ * <p>An exception from {@link #decode} leaves the tuple without a context. 
Exceptions from the other methods propagate to the
+ * caller.
+ */
+public interface TupleTracer {
+
+    /**
+     * Called once, before any other method.
+     */
+    void prepare(Map<String, Object> topoConf, WorkerTopologyContext context);

Review Comment:
   The interface defines `prepare` but no counterpart that is called on worker 
shutdown, so an implementation has no opportunity to flush or release 
resources. This is irrelevant with the agent, but not for an SDK registered by 
the application, nor in local mode with several topologies in one JVM. I 
suggest adding `default void cleanup() {}` before the interface becomes public 
API.



##########
storm-client/src/jvm/org/apache/storm/executor/spout/SpoutExecutor.java:
##########
@@ -379,6 +385,12 @@ public void failSpoutMsg(SpoutExecutor executor, Task 
taskData, Long timeDelta,
             if (executor.getIsDebug()) {
                 LOG.info("SPOUT Failing {} : {} REASON: {}", 
tupleInfo.getRootId(), tupleInfo, reason);
             }
+            Object traceContext = tupleInfo.getTraceContext();
+            if (traceContext != null) {
+                TupleTracer.Outcome outcome = "TIMEOUT".equals(reason) ? 
TupleTracer.Outcome.TIMEOUT : TupleTracer.Outcome.FAIL;

Review Comment:
   Minor: the outcome is derived from the literal reason passed by the expiry 
callback of the pending map. A constant shared by both places would make this 
dependency explicit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to