// Copyright 2016 Dino Wernli. All Rights Reserved. See LICENSE for licensing terms.

package com.github.dinowernli.grpc.prometheus;

import java.time.Clock;
import java.time.Instant;
import java.util.Optional;

import io.grpc.ForwardingServerCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.ServerCall;
import io.grpc.Status;
import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
import io.prometheus.client.SimpleCollector;

/**
 * A {@link ForwardingServerCall} which keeps track of relevant stats for a single call and
 * reports them to Prometheus.
 */
class MonitoringForwardingServerCall<S> extends ForwardingServerCall.SimpleForwardingServerCall<S> {
  private static final long MILLIS_PER_SECOND = 1000L;

  private static final Counter serverStartedCounter = Counter.build()
      .namespace("grpc")
      .subsystem("server")
      .name("started_total")
      .labelNames("grpc_type", "grpc_service", "grpc_method")
      .help("Total number of RPCs started on the server.")
      .register();

  private static final Counter serverHandledCounter = Counter.build()
      .namespace("grpc")
      .subsystem("server")
      .name("handled_total")
      .labelNames("grpc_type", "grpc_service", "grpc_method", "code")
      .help("Total number of RPCs completed on the server, regardless of success or failure.")
      .register();

  private static final Histogram serverHandledLatencySecondsHistogram = Histogram.build()
      .namespace("grpc")
      .subsystem("server")
      .name("handled_total")
      .labelNames("grpc_type", "grpc_service", "grpc_method")
      .help("Histogram of response latency (seconds) of gRPC that had been application-level " +
          "handled by the server.")
      .register();

  private static final Counter serverStreamMessagesReceived = Counter.build()
      .namespace("grpc")
      .subsystem("server")
      .name("msg_received_total")
      .labelNames("grpc_type", "grpc_service", "grpc_method")
      .help("Total number of RPCs started on the server.")
      .register();

  private static final Counter serverStreamMessagesSent = Counter.build()
      .namespace("grpc")
      .subsystem("server")
      .name("msg_received_total")
      .labelNames("grpc_type", "grpc_service", "grpc_method")
      .help("Total number of gRPC stream messages sent by the server.")
      .register();


  private final Clock clock;
  private final String methodName;
  private final MethodType methodType;
  private final String serviceName;

  private final Optional<Instant> startInstant;

  public static <R, S> ServerCall<S> create(
      ServerCall<S> call, Clock clock, MethodDescriptor<R, S> method) {
    String serviceName = MethodDescriptor.extractFullServiceName(method.getFullMethodName());
    String methodName = method.getFullMethodName();
    return new MonitoringForwardingServerCall<S>(
        call, clock, methodName, serviceName, method.getType());
  }

  MonitoringForwardingServerCall(
      ServerCall<S> call,
      Clock clock,
      String methodName,
      String serviceName,
      MethodType methodType) {
    super(call);
    this.clock = clock;
    this.methodName = methodName;
    this.serviceName = serviceName;
    this.methodType = methodType;
    this.startInstant = Optional.of(clock.instant());

    reportStartMetrics();
  }

  @Override
  public void close(Status status, Metadata responseHeaders) {
    reportEndMetrics(status);
    super.close(status, responseHeaders);
  }

  @Override
  public void sendMessage(S message) {
    if (methodType == MethodType.SERVER_STREAMING || methodType == MethodType.BIDI_STREAMING) {
      addLabels(serverStreamMessagesSent).inc();
    }
    super.sendMessage(message);

    // TODO(dino): Figure out how to count the number of messages received.
  }

  private void reportStartMetrics() {
    addLabels(serverStartedCounter).inc();
  }

  private void reportEndMetrics(Status status) {
    String codeString = status.getCode().toString();
    double latencySeconds =
        clock.millis() - startInstant.get().toEpochMilli() / (double) MILLIS_PER_SECOND;
    addLabels(serverHandledCounter, codeString).inc();
    addLabels(serverHandledLatencySecondsHistogram, codeString).observe(latencySeconds);
  }

  private <T> T addLabels(SimpleCollector<T> collector, String... labels) {
    String[] allLabels = new String[labels.length + 3];
    allLabels[0] = methodType.toString();
    allLabels[1] = serviceName;
    allLabels[2] = methodName;
    for (int i = 0; i < labels.length; ++i) {
      allLabels[i + 3] = labels[i];
    }
    return collector.labels(allLabels);
  }
}
@@ -177,16 +177,97 @@
 nstant;%0A
+import java.util.ArrayList;%0Aimport java.util.Collections;%0Aimport java.util.List;%0A
 import j
@@ -4489,16 +4489,20 @@
     
+List%3C
 String
-%5B%5D
+%3E
  all
@@ -4518,39 +4518,46 @@
 new 
-String%5Blabels.length + 3%5D;%0A    
+ArrayList%3C%3E();%0A    Collections.addAll(
 allL
@@ -4565,13 +4565,9 @@
 bels
-%5B0%5D =
+,
  met
@@ -4588,60 +4588,22 @@
 ng()
-;%0A    allLabels%5B1%5D = serviceName;%0A    allLabels%5B2%5D =
+, serviceName,
  met
@@ -4613,98 +4613,54 @@
 Name
+)
 ;%0A    
-for (int i = 0; i %3C labels.length; ++i) %7B%0A      allLabels%5Bi + 3%5D =
+Collections.addAll(allLabels,
  labels
-%5Bi%5D
+)
 ;%0A
-    %7D%0A
     
@@ -4688,20 +4688,43 @@
 ls(allLabels
+.toArray(new String%5B0%5D)
 );%0A  %7D%0A%7D

Replace the label array with a list.