dongjoon-hyun commented on code in PR #949:
URL: 
https://github.com/apache/spark-kubernetes-operator/pull/949#discussion_r4437461938


##########
docs/migration_guide.md:
##########
@@ -125,9 +125,23 @@ affect building the project, CI, tests or examples.
   `kubernetes_client_http_response_latency_seconds_sum`, is the sum of all 
recorded values. 1.0
   exported the mean of roughly the last 5 minutes multiplied by the count 
instead, which can
   decrease between scrapes, so `rate()` of it and the average derived from it 
were wrong. Also, the
-  `0.5` and `0.999` quantiles of the `operator_sdk_*` histograms are the 
median and the 99.9th
+  `0.5` and `0.999` quantiles of the operator histograms and timers are the 
median and the 99.9th
   percentile instead of the mean and the 99th percentile. A histogram or timer 
of a custom metrics
   source has no `_sum` unless it is a `SummingHistogram` or a `SummingTimer`.
+- Since 1.1.0, the `operator.sdk` metrics source times the controller 
executions, i.e. the
+  reconciliations and cleanups, with timers instead of histograms, so the 
source has no histograms
+  anymore. The timers record nanoseconds. 1.0 recorded whole seconds, so an 
execution under one
+  second, which most are, was recorded as `0`, and the quantiles and `_sum` of 
these histograms
+  were mostly `0`. With the default text-based Prometheus format, the timers 
are exported in
+  seconds with a `_duration_seconds` suffix regardless of
+  `spark.kubernetes.operator.metrics.sanitizePrometheusMetricsNameEnabled`, 
e.g.
+  
`operator_sdk_sparkapplication_sparkappreconciler_reconcile_both_duration_seconds`
 instead of
+  `operator_sdk_sparkapplication_sparkappreconciler_reconcile_both`, plus 
`_m1_rate`, `_m5_rate`

Review Comment:
   `reconcile_both` seems to come from `successTypeName` of 
`TestingExecutionBase` in the test. JOSDK names a successful reconciliation 
`resource`, `status` or `both` depending on the returned `UpdateControl`, and 
`SparkAppReconciler` and `SparkClusterReconciler` always return 
`UpdateControl.noUpdate()`, also via `ReconcilerUtils.toUpdateControl`, which 
is `resource`. So neither 1.0 nor 1.1 exports `..._reconcile_both`, and users 
searching their dashboards for these names would find nothing and may conclude 
that they are not affected. Could you use 
`operator_sdk_sparkapplication_sparkappreconciler_reconcile_resource_duration_seconds`
 and `operator_sdk_sparkapplication_sparkappreconciler_reconcile_resource` 
instead? FYI, the other outcomes are `reconcile_failure`, `cleanup_delete`, 
`cleanup_finalizernotremoved` and `cleanup_failure`.



##########
docs/migration_guide.md:
##########
@@ -125,9 +125,23 @@ affect building the project, CI, tests or examples.
   `kubernetes_client_http_response_latency_seconds_sum`, is the sum of all 
recorded values. 1.0
   exported the mean of roughly the last 5 minutes multiplied by the count 
instead, which can
   decrease between scrapes, so `rate()` of it and the average derived from it 
were wrong. Also, the
-  `0.5` and `0.999` quantiles of the `operator_sdk_*` histograms are the 
median and the 99.9th
+  `0.5` and `0.999` quantiles of the operator histograms and timers are the 
median and the 99.9th
   percentile instead of the mean and the 99th percentile. A histogram or timer 
of a custom metrics
   source has no `_sum` unless it is a `SummingHistogram` or a `SummingTimer`.
+- Since 1.1.0, the `operator.sdk` metrics source times the controller 
executions, i.e. the
+  reconciliations and cleanups, with timers instead of histograms, so the 
source has no histograms
+  anymore. The timers record nanoseconds. 1.0 recorded whole seconds, so an 
execution under one
+  second, which most are, was recorded as `0`, and the quantiles and `_sum` of 
these histograms
+  were mostly `0`. With the default text-based Prometheus format, the timers 
are exported in
+  seconds with a `_duration_seconds` suffix regardless of
+  `spark.kubernetes.operator.metrics.sanitizePrometheusMetricsNameEnabled`, 
e.g.
+  
`operator_sdk_sparkapplication_sparkappreconciler_reconcile_both_duration_seconds`
 instead of
+  `operator_sdk_sparkapplication_sparkappreconciler_reconcile_both`, plus 
`_m1_rate`, `_m5_rate`
+  and `_m15_rate` gauges per timer. With

Review Comment:
   `formatTimer` also exports `<name>_count` under `# TYPE <name> gauge`, which 
keeps the name and the value of the `_count` of the 1.0 summary, e.g. 
`operator_sdk_sparkapplication_sparkappreconciler_reconcile_resource_count`. So 
only the 6 quantiles and `_sum` move to `_duration_seconds`, and the queries on 
`_count`, e.g. `rate(..._count[5m])`, keep working. Could you mention it, so 
that users don't rewrite them unnecessarily? It also means 4 extra series per 
timer (8 -> 12), not 3.



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetrics.java:
##########
@@ -316,10 +341,6 @@ public void reconciliationSucceeded(HasMetadata resource, 
Map<String, Object> me
         .dec();
   }
 
-  private long toSeconds(long startTimeInMilliseconds) {

Review Comment:
   Like `toSeconds`, `BaseOperatorSource.getHistogram` and its `histograms` map 
lose their last callers with this PR, and the 
`assertInstanceOf(SummingHistogram.class, ...)` removed from 
`OperatorJosdkMetricsTest` was their only test. Shall we remove them as well? 
Or, since `getHistogram` is a `protected` method of a public class, we may keep 
it with a direct test instead, because nothing would catch a change that drops 
its `SummingHistogram::new`, i.e. the `_sum` of custom sources.



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetrics.java:
##########
@@ -192,6 +191,32 @@ public <T> T 
timeControllerExecution(ControllerExecution<T> execution) throws Ex
       }
       throw e;
     }
+    final long durationNanos = clock.nanoTime() - startTime;
+    final String successType = execution.successTypeName(result);
+    if (resourceClass.isPresent()) {
+      String metricsPrefix = getMetricNamePrefix(resourceClass.get());
+      recordDuration(metricsPrefix, namespaceOptional, durationNanos, name, 
execName, successType);
+      getCounter(metricsPrefix, name, execName, SUCCESS, successType).inc();
+      if (namespaceOptional.isPresent()) {
+        getCounter(metricsPrefix, namespaceOptional.get(), name, execName, 
SUCCESS, successType)
+            .inc();
+      }
+    }
+    return result;
+  }
+
+  private void recordDuration(
+      String metricsPrefix,
+      Optional<String> namespaceOptional,
+      long durationNanos,
+      String name,
+      String execName,
+      String type) {
+    getTimer(metricsPrefix, name, execName, type).update(durationNanos, 
TimeUnit.NANOSECONDS);

Review Comment:
   Following up on 3 of my previous review, which is still open: the 4 extra 
series per timer carry no new information. `<name>_count` equals 
`<name>_duration_seconds_count`, and the `_m1_rate`, `_m5_rate` and `_m15_rate` 
gauges can be derived with `rate(<name>_duration_seconds_count[...])`.
   
   For scale, the number of timers is 2 kinds x (1 + watched namespaces) x 5 
outcomes (`reconcile_resource`, `reconcile_failure`, `cleanup_delete`, 
`cleanup_finalizernotremoved` and `cleanup_failure`). So an operator watching 
100 namespaces has up to about 1,010 timers, i.e. about 4,040 more series per 
scrape. With `prometheusTextBasedFormatEnabled=false`, it is 15 instead of 11 
series per metric.



##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetricsTest.java:
##########
@@ -68,29 +76,57 @@ void testTimeControllerExecution() throws Exception {
     operatorMetrics.timeControllerExecution(successExecution);
     Map<String, Metric> metrics = 
operatorMetrics.metricRegistry().getMetrics();
     assertEquals(4, metrics.size());
-    assertInstanceOf(
-        SummingHistogram.class, 
metrics.get("sparkapplication.test-controller.reconcile.both"));
-    
assertTrue(metrics.containsKey("sparkapplication.testns.test-controller.reconcile.both"));
+    // The execution took 10 ms, which is recorded with sub-second precision 
in nanoseconds.
+    long durationNanos = Duration.ofMillis(10).toNanos();
+    assertEquals(
+        durationNanos, recordedNanos(metrics, 
"sparkapplication.test-controller.reconcile.both"));
+    assertEquals(
+        durationNanos,
+        recordedNanos(metrics, 
"sparkapplication.testns.test-controller.reconcile.both"));
     
assertTrue(metrics.containsKey("sparkapplication.test-controller.reconcile.success.both"));
     assertTrue(
         
metrics.containsKey("sparkapplication.testns.test-controller.reconcile.success.both"));
 
     FooTestingExecutionBase<SparkApplication> failedExecution = new 
FooTestingExecutionBase<>();
-    try {
-      operatorMetrics.timeControllerExecution(failedExecution);
-    } catch (Exception e) {
-      assertEquals("Foo exception", e.getMessage());
-      assertEquals(8, metrics.size());
-      
assertTrue(metrics.containsKey("sparkapplication.test-controller.reconcile.failure"));
-      assertTrue(
-          metrics.containsKey(
-              "sparkapplication.test-controller.reconcile.failure.exception"
-                  + ".nosuchfieldexception"));
-      
assertTrue(metrics.containsKey("sparkapplication.testns.test-controller.reconcile.failure"));
-      assertTrue(
-          metrics.containsKey(
-              "sparkapplication.testns.test-controller.reconcile.failure."
-                  + "exception.nosuchfieldexception"));
+    Exception e =
+        assertThrows(
+            NoSuchFieldException.class,
+            () -> operatorMetrics.timeControllerExecution(failedExecution));
+    assertEquals("Foo exception", e.getMessage());
+    assertEquals(8, metrics.size());
+    long failureNanos = Duration.ofMillis(5).toNanos();
+    assertEquals(
+        failureNanos,
+        recordedNanos(metrics, 
"sparkapplication.test-controller.reconcile.failure"));
+    assertTrue(
+        metrics.containsKey(
+            "sparkapplication.test-controller.reconcile.failure.exception"
+                + ".nosuchfieldexception"));
+    assertEquals(
+        failureNanos,
+        recordedNanos(metrics, 
"sparkapplication.testns.test-controller.reconcile.failure"));
+    assertTrue(
+        metrics.containsKey(
+            "sparkapplication.testns.test-controller.reconcile.failure."
+                + "exception.nosuchfieldexception"));
+  }
+
+  @Test
+  void testTimeControllerExecutionIsExportedInSeconds() throws Exception {
+    operatorMetrics.timeControllerExecution(new TestingExecutionBase<>());
+    TestingPrometheusPullModelHandler handler =
+        new 
TestingPrometheusPullModelHandler(operatorMetrics.metricRegistry());

Review Comment:
   nit: This formats the source registry directly, so the names lack the 
`operator_sdk_` prefix, and the test doesn't cover how 
`MetricsSystem.registerSource` exposes these timers through the parent 
registry. For example, if the registration exposed a wrapper or a copy instead 
of the `SummingTimer` itself, `..._duration_seconds_sum` would silently 
disappear in production while this test still passes. How about formatting a 
parent registry instead, e.g. after 
`parent.register(operatorMetrics.sourceName(), 
operatorMetrics.metricRegistry())`, and asserting the documented 
`operator_sdk_..._duration_seconds` names?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetrics.java:
##########
@@ -192,6 +191,32 @@ public <T> T 
timeControllerExecution(ControllerExecution<T> execution) throws Ex
       }
       throw e;
     }
+    final long durationNanos = clock.nanoTime() - startTime;
+    final String successType = execution.successTypeName(result);
+    if (resourceClass.isPresent()) {
+      String metricsPrefix = getMetricNamePrefix(resourceClass.get());
+      recordDuration(metricsPrefix, namespaceOptional, durationNanos, name, 
execName, successType);
+      getCounter(metricsPrefix, name, execName, SUCCESS, successType).inc();
+      if (namespaceOptional.isPresent()) {
+        getCounter(metricsPrefix, namespaceOptional.get(), name, execName, 
SUCCESS, successType)
+            .inc();
+      }
+    }
+    return result;
+  }
+
+  private void recordDuration(
+      String metricsPrefix,
+      Optional<String> namespaceOptional,
+      long durationNanos,
+      String name,
+      String execName,
+      String type) {
+    getTimer(metricsPrefix, name, execName, type).update(durationNanos, 
TimeUnit.NANOSECONDS);
+    if (namespaceOptional.isPresent()) {
+      getTimer(metricsPrefix, namespaceOptional.get(), name, execName, type)
+          .update(durationNanos, TimeUnit.NANOSECONDS);

Review Comment:
   nit, outside this PR: `PrometheusPullModelHandler.formatTimer` calls 
`timer.getSnapshot()` twice per timer on every scrape, once for the `null` 
check and once for the values, and each call copies and sorts up to 1028 
samples of the reservoir. Now that these reservoirs hold distinct nanosecond 
values instead of mostly `0`, the discarded snapshot costs more to sort, on the 
HTTP handler thread. Taking the snapshot once into a local variable would avoid 
it. `formatHistogram` has the same pattern.



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetrics.java:
##########
@@ -134,7 +144,9 @@ public void eventReceived(Event event, Map<String, Object> 
metadata) {
   }
 
   /**
-   * Times the execution of a controller operation and records success or 
failure metrics.
+   * Times the execution of a controller operation and records success or 
failure metrics. The
+   * duration is measured with the monotonic clock in nanoseconds and recorded 
in timers, which
+   * {@link org.apache.spark.k8s.operator.metrics.PrometheusPullModelHandler} 
exports in seconds.

Review Comment:
   Like the migration guide, this holds only with the default text-based 
format. With 
`spark.kubernetes.operator.metrics.prometheusTextBasedFormatEnabled=false`, 
`handle()` falls back to Spark's `PrometheusServlet`, which exports these 
timers in raw nanoseconds, e.g. `..._50thPercentile{type="timers"} 1.0E7` for 
10 ms. How about adding "with the default text-based format"?



##########
docs/migration_guide.md:
##########
@@ -125,9 +125,23 @@ affect building the project, CI, tests or examples.
   `kubernetes_client_http_response_latency_seconds_sum`, is the sum of all 
recorded values. 1.0
   exported the mean of roughly the last 5 minutes multiplied by the count 
instead, which can
   decrease between scrapes, so `rate()` of it and the average derived from it 
were wrong. Also, the
-  `0.5` and `0.999` quantiles of the `operator_sdk_*` histograms are the 
median and the 99.9th
+  `0.5` and `0.999` quantiles of the operator histograms and timers are the 
median and the 99.9th
   percentile instead of the mean and the 99th percentile. A histogram or timer 
of a custom metrics
   source has no `_sum` unless it is a `SummingHistogram` or a `SummingTimer`.
+- Since 1.1.0, the `operator.sdk` metrics source times the controller 
executions, i.e. the
+  reconciliations and cleanups, with timers instead of histograms, so the 
source has no histograms
+  anymore. The timers record nanoseconds. 1.0 recorded whole seconds, so an 
execution under one
+  second, which most are, was recorded as `0`, and the quantiles and `_sum` of 
these histograms
+  were mostly `0`. With the default text-based Prometheus format, the timers 
are exported in
+  seconds with a `_duration_seconds` suffix regardless of
+  `spark.kubernetes.operator.metrics.sanitizePrometheusMetricsNameEnabled`, 
e.g.
+  
`operator_sdk_sparkapplication_sparkappreconciler_reconcile_both_duration_seconds`
 instead of
+  `operator_sdk_sparkapplication_sparkappreconciler_reconcile_both`, plus 
`_m1_rate`, `_m5_rate`
+  and `_m15_rate` gauges per timer. With
+  `spark.kubernetes.operator.metrics.prometheusTextBasedFormatEnabled=false`, 
Spark's
+  `PrometheusServlet` exports them as `{type="timers"}` in raw nanoseconds, 
and other Spark sinks,
+  e.g. JMX, Console or CSV, apply their own duration unit. Update queries and 
dashboards

Review Comment:
   Thank you for adding this. Besides the unit, these sinks now see timers 
instead of histograms. For example, since `JmxReporter` puts the metric type 
into the `ObjectName`, `JmxSink` registers these MBeans as 
`metrics:name=operator.sdk.<...>,type=timers` instead of `type=histograms`, 
with a different set of attributes, so a `jmx_exporter` rule matching 
`type=histograms` silently stops matching. Likewise, the Console and CSV sinks 
report them as timers with different fields. Could you mention it as well?



##########
docs/migration_guide.md:
##########
@@ -125,9 +125,23 @@ affect building the project, CI, tests or examples.
   `kubernetes_client_http_response_latency_seconds_sum`, is the sum of all 
recorded values. 1.0
   exported the mean of roughly the last 5 minutes multiplied by the count 
instead, which can
   decrease between scrapes, so `rate()` of it and the average derived from it 
were wrong. Also, the
-  `0.5` and `0.999` quantiles of the `operator_sdk_*` histograms are the 
median and the 99.9th
+  `0.5` and `0.999` quantiles of the operator histograms and timers are the 
median and the 99.9th

Review Comment:
   Thank you for updating this. However, "the operator histograms and timers" 
now also covers metrics whose quantiles did not change. In 
[1.0.0](https://github.com/apache/spark-kubernetes-operator/blob/1.0.0/spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/PrometheusPullModelHandler.java#L245-L280),
 `formatTimer` already exported the median and the 99.9th percentile, and so 
did `formatHistogram` for a name containing `nanos`. Only the other histograms 
used the mean and the 99th percentile.
   
   After this PR, the only built-in histogram is 
`kubernetes.client.http.response.latency.nanos`, and the `sparkapp.latency.*` 
metrics were timers in 1.0 as well, so this sentence matches no built-in 
metric. For example, 
`kubernetes_client_http_response_latency_seconds{quantile="0.5"}` was the 
median in 1.0 too. Shall we limit it to the histograms of custom metrics 
sources whose names don't contain `nanos`?



##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/metrics/source/OperatorJosdkMetricsTest.java:
##########
@@ -55,11 +61,13 @@ class OperatorJosdkMetricsTest {
           "test-controller-name");
   private static final String controllerName = "test-controller";
 
+  private ManualClock clock;
   private OperatorJosdkMetrics operatorMetrics;
 
   @BeforeEach
   void setup() {
-    operatorMetrics = new OperatorJosdkMetrics();
+    clock = new ManualClock();

Review Comment:
   Since `new ManualClock()` starts at `0`, `startTime` is `0` in the first 
execution of each test, so both tests still pass if the success path records 
`clock.nanoTime()` instead of `clock.nanoTime() - startTime`. Only the failure 
path, which starts at 10 ms, would catch such a regression, while in 
production, `System.nanoTime()` has an arbitrary origin and every successful 
execution would record a huge duration. How about starting at a non-zero time, 
e.g. `new ManualClock(1000)`? The expected durations stay the same.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to