GenericContainer<?> createZipkinContainer(@Nullable GenericContainer<?> storage)
    throws Exception {
    Map<String, String> env = new LinkedHashMap<>();
    if (storage != null) {
      String name = storage.getLabels().get("name");
      String host = name;
      int port = storage.getExposedPorts().get(0);
      String address = host + ":" + port;

      env.put("STORAGE_TYPE", storage.getLabels().get("storageType"));
      switch (name) {
        case "elasticsearch":
          env.put("ES_HOSTS", "http://" + address);
          break;
        case "cassandra":
        case "cassandra3":
          env.put("CASSANDRA_CONTACT_POINTS", address);
          break;
        case "mysql":
          env.put("MYSQL_HOST", host);
          env.put("MYSQL_TCP_PORT", Integer.toString(port));
          env.put("MYSQL_USER", "zipkin");
          env.put("MYSQL_PASS", "zipkin");
          break;
        default:
          throw new IllegalArgumentException("Unknown storage " + name +
            ". Update startZipkin to map it to properties.");
      }
    }

    final GenericContainer<?> zipkin;
    if (RELEASE_VERSION == null) {
      zipkin = new GenericContainer<>(parse("ghcr.io/openzipkin/java:21.0.6_p7"));
      List<String> classpath = new ArrayList<>();
      for (String item : System.getProperty("java.class.path").split(File.pathSeparator)) {
        Path path = Paths.get(item);
        final String containerPath;
        if (Files.isDirectory(path)) {
          Path root = path.getParent();
          while (root != null) {
            try (Stream<Path> f = Files.list(root)) {
              if (f.anyMatch(p -> p.getFileName().toString().equals("mvnw"))) {
                break;
              }
            }
            root = root.getParent();
          }
          containerPath = root.relativize(path).toString().replace('\\', '/');
        } else {
          containerPath = path.getFileName().toString();
        }
        // Test containers currently doesn't support copying in a path with subdirectories that
        // need to be created, so we mangle directory structure into a single directory with
        // hyphens.
        String classPathItem = "/classpath-" + containerPath.replace('/', '-');
        zipkin.withCopyFileToContainer(MountableFile.forHostPath(item), classPathItem);
        classpath.add(classPathItem);
      }
      zipkin.withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("java"));
      zipkin.setCommand("-cp", String.join(":", classpath), "zipkin.server.ZipkinServer");
      // Don't fail on classpath problem from missing lens, as we don't use it.
      env.put("ZIPKIN_UI_ENABLED", "false");
    } else {
      zipkin = new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin:" + RELEASE_VERSION));
    }

    zipkin
      .withNetwork(Network.SHARED)
      .withNetworkAliases("zipkin")
      .withExposedPorts(9411)
      .withEnv(env)
      .waitingFor(Wait.forHttp("/health"));
    containers.add(zipkin);
    return zipkin;
  }
This benchmark runs zipkin-server, storage backends, an example application, prometheus, grafana,
and wrk using docker to generate a performance report.

<p>Currently there are two environment variable knobs

<ul>
  <li>
    RELEASE_VERSION - specify to a released zipkin server. If unspecified, will use the current code,
    i.e., the code currently displayed in your IDE.
  </li>
  <li>
    ZIPKIN_BENCHMARK_WAIT - set to true to have the benchmark wait until user manually terminates at the end.
    Useful to manually inspect prometheus / grafana.
  </li>
</ul>

<p>Note to Windows laptop users: you will probably need to restart the Docker daemon before a
session of benchmarks. Docker containers seem to have time get out of sync when a computer sleeps
until you restart the daemon - this causes Prometheus metrics to not scrape properly.
benchmarks/src/test/java/zipkin2/server/ServerIntegratedBenchmark.java