diff --git a/src/envoy/http/mixer/control_factory.h b/src/envoy/http/mixer/control_factory.h
index 2dc21057b98..0d5a840e620 100644
--- a/src/envoy/http/mixer/control_factory.h
+++ b/src/envoy/http/mixer/control_factory.h
@@ -19,6 +19,7 @@
 #include "envoy/local_info/local_info.h"
 #include "src/envoy/http/mixer/control.h"
 #include "src/envoy/utils/stats.h"
+#include "src/istio/utils/logger.h"
 
 namespace Envoy {
 namespace Http {
@@ -51,6 +52,10 @@ class ControlFactory : public Logger::Loggable<Logger::Id::config> {
       return std::make_shared<Control>(control_data, cm, dispatcher, random,
                                        scope, local_info);
     });
+
+    // All MIXER_DEBUG(), MIXER_WARN(), etc log messages will be passed to
+    // ENVOY_LOG().
+    istio::utils::setLogger(std::make_unique<LoggerAdaptor>());
   }
 
   Control& control() { return tls_->getTyped<Control>(); }
@@ -62,6 +67,44 @@ class ControlFactory : public Logger::Loggable<Logger::Id::config> {
     return {ALL_MIXER_FILTER_STATS(POOL_COUNTER_PREFIX(scope, name))};
   }
 
+  class LoggerAdaptor : public istio::utils::Logger,
+                        Envoy::Logger::Loggable<Envoy::Logger::Id::filter> {
+    virtual bool isLoggable(istio::utils::Logger::Level level) override {
+      switch (level) {
+        case istio::utils::Logger::Level::DEBUG_:
+          return ENVOY_LOG_CHECK_LEVEL(debug);
+        case istio::utils::Logger::Level::TRACE_:
+          return ENVOY_LOG_CHECK_LEVEL(trace);
+        case istio::utils::Logger::Level::INFO_:
+          return ENVOY_LOG_CHECK_LEVEL(info);
+        case istio::utils::Logger::Level::WARN_:
+          return ENVOY_LOG_CHECK_LEVEL(warn);
+        case istio::utils::Logger::Level::ERROR_:
+          return ENVOY_LOG_CHECK_LEVEL(error);
+      }
+    }
+
+    virtual void writeBuffer(istio::utils::Logger::Level level,
+                             const char* buffer) override {
+      switch (level) {
+        case istio::utils::Logger::Level::DEBUG_:
+          ENVOY_LOGGER().debug(buffer);
+          break;
+        case istio::utils::Logger::Level::TRACE_:
+          ENVOY_LOGGER().trace(buffer);
+          break;
+        case istio::utils::Logger::Level::INFO_:
+          ENVOY_LOGGER().info(buffer);
+          break;
+        case istio::utils::Logger::Level::WARN_:
+          ENVOY_LOGGER().warn(buffer);
+          break;
+        case istio::utils::Logger::Level::ERROR_:
+          ENVOY_LOGGER().error(buffer);
+      }
+    }
+  };
+
   // The control data object
   ControlDataSharedPtr control_data_;
   // Thread local slot.
diff --git a/src/istio/control/client_context_base.cc b/src/istio/control/client_context_base.cc
index c2ff0ef551e..2c9690d2564 100644
--- a/src/istio/control/client_context_base.cc
+++ b/src/istio/control/client_context_base.cc
@@ -17,6 +17,7 @@
 #include "include/istio/mixerclient/check_response.h"
 #include "include/istio/utils/attribute_names.h"
 #include "include/istio/utils/attributes_builder.h"
+#include "src/istio/utils/logger.h"
 
 using ::google::protobuf::util::Status;
 using ::istio::mixer::v1::config::client::NetworkFailPolicy;
@@ -98,17 +99,17 @@ CancelFunc ClientContextBase::SendCheck(TransportCheckFunc transport,
     on_done(check_response_info);
   };
 
-  // TODO: add debug message
-  // GOOGLE_LOG(INFO) << "Check attributes: " <<
-  // request->attributes->DebugString();
+  MIXER_DEBUG("Check attributes: %s",
+              request->attributes->DebugString().c_str());
+
   return mixer_client_->Check(*request->attributes, request->quotas, transport,
                               local_on_done);
 }
 
 void ClientContextBase::SendReport(const RequestContext& request) {
-  // TODO: add debug message
-  // GOOGLE_LOG(INFO) << "Report attributes: " <<
-  // request.attributes->DebugString();
+  MIXER_DEBUG("Report attributes: %s",
+              request.attributes->DebugString().c_str());
+
   mixer_client_->Report(*request.attributes);
 }
 
diff --git a/src/istio/mixerclient/check_cache.cc b/src/istio/mixerclient/check_cache.cc
index 2f10557ce2a..18a2cad2879 100644
--- a/src/istio/mixerclient/check_cache.cc
+++ b/src/istio/mixerclient/check_cache.cc
@@ -15,6 +15,7 @@
 
 #include "src/istio/mixerclient/check_cache.h"
 #include "include/istio/utils/protobuf.h"
+#include "src/istio/utils/logger.h"
 
 using namespace std::chrono;
 using ::google::protobuf::util::Status;
@@ -147,9 +148,10 @@ Status CheckCache::CacheResponse(const Attributes &attributes,
   }
   utils::HashType signature;
   if (!referenced.Signature(attributes, "", &signature)) {
-    GOOGLE_LOG(ERROR) << "Response referenced mismatchs with request";
-    GOOGLE_LOG(ERROR) << "Request attributes: " << attributes.DebugString();
-    GOOGLE_LOG(ERROR) << "Referenced attributes: " << referenced.DebugString();
+    MIXER_WARN(
+        "Response referenced does not match request.  Request attributes: "
+        "%s.  Referenced attributes: %s",
+        attributes.DebugString().c_str(), referenced.DebugString().c_str());
     return ConvertRpcStatus(response.precondition().status());
   }
 
@@ -157,8 +159,8 @@ Status CheckCache::CacheResponse(const Attributes &attributes,
   utils::HashType hash = referenced.Hash();
   if (referenced_map_.find(hash) == referenced_map_.end()) {
     referenced_map_[hash] = referenced;
-    GOOGLE_LOG(INFO) << "Add a new Referenced for check cache: "
-                     << referenced.DebugString();
+    MIXER_DEBUG("Add a new Referenced for check cache: %s",
+                referenced.DebugString().c_str());
   }
 
   CheckLRUCache::ScopedLookup lookup(cache_.get(), signature);
diff --git a/src/istio/mixerclient/quota_cache.cc b/src/istio/mixerclient/quota_cache.cc
index 4d77a038fff..4cb6f904494 100644
--- a/src/istio/mixerclient/quota_cache.cc
+++ b/src/istio/mixerclient/quota_cache.cc
@@ -15,6 +15,7 @@
 
 #include "src/istio/mixerclient/quota_cache.h"
 #include "include/istio/utils/protobuf.h"
+#include "src/istio/utils/logger.h"
 
 using namespace std::chrono;
 using ::google::protobuf::util::Status;
@@ -96,6 +97,7 @@ bool QuotaCache::CheckResult::BuildRequest(CheckRequest* request) {
     }
   }
   if (!rejected_quota_names.empty()) {
+    MIXER_DEBUG("Quota is exhausted for: %s", rejected_quota_names.c_str());
     status_ =
         Status(Code::RESOURCE_EXHAUSTED,
                std::string("Quota is exhausted for: ") + rejected_quota_names);
@@ -118,8 +120,8 @@ void QuotaCache::CheckResult::SetResponse(const Status& status,
         if (it != quotas.end()) {
           result = &it->second;
         } else {
-          GOOGLE_LOG(ERROR)
-              << "Quota response did not have quota for: " << quota.name;
+          MIXER_WARN("Quota response did not have quota for: %s",
+                     quota.name.c_str());
         }
       }
       if (!quota.response_func(attributes, result)) {
@@ -131,6 +133,7 @@ void QuotaCache::CheckResult::SetResponse(const Status& status,
     }
   }
   if (!rejected_quota_names.empty()) {
+    MIXER_DEBUG("Quota is exhausted for: %s", rejected_quota_names.c_str());
     status_ =
         Status(Code::RESOURCE_EXHAUSTED,
                std::string("Quota is exhausted for: ") + rejected_quota_names);
@@ -218,9 +221,10 @@ void QuotaCache::SetResponse(const Attributes& attributes,
 
   utils::HashType signature;
   if (!referenced.Signature(attributes, quota_name, &signature)) {
-    GOOGLE_LOG(ERROR) << "Quota response referenced mismatchs with request";
-    GOOGLE_LOG(ERROR) << "Request attributes: " << attributes.DebugString();
-    GOOGLE_LOG(ERROR) << "Referenced attributes: " << referenced.DebugString();
+    MIXER_WARN(
+        "Quota response referenced does not match request. Request "
+        "attributes: %s, Referenced attributes: %s",
+        attributes.DebugString().c_str(), referenced.DebugString().c_str());
     return;
   }
 
@@ -235,8 +239,8 @@ void QuotaCache::SetResponse(const Attributes& attributes,
   utils::HashType hash = referenced.Hash();
   if (quota_ref.referenced_map.find(hash) == quota_ref.referenced_map.end()) {
     quota_ref.referenced_map[hash] = referenced;
-    GOOGLE_LOG(INFO) << "Add a new Referenced for quota cache: " << quota_name
-                     << ", reference: " << referenced.DebugString();
+    MIXER_DEBUG("Add a new Referenced for quota cache: %s, reference: %s",
+                quota_name.c_str(), referenced.DebugString().c_str());
   }
 
   cache_->Insert(signature, quota_ref.pending_item.release(), 1);
diff --git a/src/istio/mixerclient/report_batch.cc b/src/istio/mixerclient/report_batch.cc
index 5bb07ef436f..9906eba3883 100644
--- a/src/istio/mixerclient/report_batch.cc
+++ b/src/istio/mixerclient/report_batch.cc
@@ -15,6 +15,7 @@
 
 #include "src/istio/mixerclient/report_batch.h"
 #include "include/istio/utils/protobuf.h"
+#include "src/istio/utils/logger.h"
 
 using ::google::protobuf::util::Status;
 using ::google::protobuf::util::error::Code;
@@ -25,6 +26,9 @@ using ::istio::mixer::v1::ReportResponse;
 namespace istio {
 namespace mixerclient {
 
+static std::atomic<uint32_t> REPORT_FAIL_LOG_MESSAGES{0};
+static constexpr uint32_t REPORT_FAIL_LOG_MODULUS{100};
+
 ReportBatch::ReportBatch(const ReportOptions& options,
                          TransportReportFunc transport,
                          TimerCreateFunc timer_create,
@@ -70,7 +74,12 @@ void ReportBatch::FlushWithLock() {
   transport_(request, response, [this, response](const Status& status) {
     delete response;
     if (!status.ok()) {
-      GOOGLE_LOG(ERROR) << "Mixer Report failed with: " << status.ToString();
+      if (MIXER_WARN_ENABLED &&
+          0 == REPORT_FAIL_LOG_MESSAGES++ % REPORT_FAIL_LOG_MODULUS) {
+        MIXER_WARN("Mixer Report failed with: %s", status.ToString().c_str());
+      } else {
+        MIXER_DEBUG("Mixer Report failed with: %s", status.ToString().c_str());
+      }
       if (utils::InvalidDictionaryStatus(status)) {
         compressor_.ShrinkGlobalDictionary();
       }
diff --git a/src/istio/prefetch/BUILD b/src/istio/prefetch/BUILD
index ded66da7c2e..2695206733c 100644
--- a/src/istio/prefetch/BUILD
+++ b/src/istio/prefetch/BUILD
@@ -25,6 +25,7 @@ cc_library(
     visibility = ["//visibility:public"],
     deps = [
         "//include/istio/prefetch:headers_lib",
+	"//src/istio/utils:utils_lib",
     ],
 )
 
diff --git a/src/istio/prefetch/quota_prefetch.cc b/src/istio/prefetch/quota_prefetch.cc
index ec244d2bc72..62ae5642658 100644
--- a/src/istio/prefetch/quota_prefetch.cc
+++ b/src/istio/prefetch/quota_prefetch.cc
@@ -16,28 +16,12 @@
 #include "include/istio/prefetch/quota_prefetch.h"
 #include "src/istio/prefetch/circular_queue.h"
 #include "src/istio/prefetch/time_based_counter.h"
+#include "src/istio/utils/logger.h"
 
 #include <mutex>
 
 using namespace std::chrono;
 
-// Turn this on to debug for quota_prefetch_test.cc
-// Not for debugging in production.
-#if 0
-#include <iostream>
-#define LOG(t)                                                           \
-  std::cerr << "("                                                       \
-            << duration_cast<milliseconds>(t.time_since_epoch()).count() \
-            << "):"
-#else
-// Pipe to stringstream to disable logging.
-#include <sstream>
-std::ostringstream os;
-#define LOG(t) \
-  os.clear();  \
-  os
-#endif
-
 namespace istio {
 namespace prefetch {
 namespace {
@@ -168,6 +152,10 @@ void QuotaPrefetchImpl::AttemptPrefetch(int amount, Tick t) {
   int avail = CountAvailable(t);
   int pass_count = counter_.Count(t);
   int desired = std::max(pass_count, options_.min_prefetch_amount);
+  MIXER_TRACE(
+      "Prefetch decision: available=%d, desired=%d, inflight_count=%d, "
+      "requested=%d",
+      avail, desired, inflight_count_, amount);
   if ((avail < desired / 2 && inflight_count_ == 0) || avail < amount) {
     bool use_not_granted = (avail == 0 && mode_ == OPEN);
     Prefetch(std::max(amount, desired), use_not_granted, t);
@@ -181,7 +169,7 @@ void QuotaPrefetchImpl::Prefetch(int req_amount, bool use_not_granted, Tick t) {
     slot_id = Add(req_amount, t + milliseconds(kMaxExpirationInMs));
   }
 
-  LOG(t) << "Prefetch: " << req_amount << ", id: " << slot_id << std::endl;
+  MIXER_DEBUG("Prefetch amount %d for slotid: %lu", req_amount, slot_id);
 
   last_prefetch_time_ = t;
   ++inflight_count_;
@@ -225,7 +213,7 @@ int QuotaPrefetchImpl::Substract(int delta, Tick t) {
       }
     } else {
       if (n->available > 0) {
-        LOG(t) << "Expired:" << n->available << std::endl;
+        MIXER_DEBUG("Expired: %d", n->available);
       }
     }
     queue_.Pop();
@@ -240,9 +228,8 @@ void QuotaPrefetchImpl::OnResponse(SlotId slot_id, int req_amount,
   std::lock_guard<std::mutex> lock(mutex_);
   --inflight_count_;
 
-  LOG(t) << "OnResponse: req:" << req_amount << ", resp: " << resp_amount
-         << ", expire: " << expiration.count() << ", id: " << slot_id
-         << std::endl;
+  MIXER_DEBUG("OnResponse: req: %d, resp: %d, expire: %ld, id: %lu", req_amount,
+              resp_amount, expiration.count(), slot_id);
 
   // resp_amount of -1 indicates any network failures.
   // Use fail open policy to handle any netowrk failures.
@@ -301,7 +288,7 @@ bool QuotaPrefetchImpl::Check(int amount, Tick t) {
     }
   }
   if (!ret) {
-    LOG(t) << "Rejected amount: " << amount << std::endl;
+    MIXER_DEBUG("Rejected amount: %d", amount);
   }
   return ret;
 }
diff --git a/src/istio/utils/BUILD b/src/istio/utils/BUILD
index 4cc7ff72413..b6bced64571 100644
--- a/src/istio/utils/BUILD
+++ b/src/istio/utils/BUILD
@@ -18,11 +18,13 @@ cc_library(
     name = "utils_lib",
     srcs = [
         "local_attributes.cc",
+        "logger.cc",
         "protobuf.cc",
         "status.cc",
         "utils.cc"
     ],
     hdrs = [
+        "logger.h",
         "utils.h",
     ],
     visibility = ["//visibility:public"],
@@ -60,6 +62,16 @@ cc_test(
     ],
 )
 
+cc_test(
+    name = "logger_test",
+    size = "small",
+    srcs = ["logger_test.cc"],
+    deps = [
+        ":utils_lib",
+        "//external:googletest_main",
+    ],
+)
+
 cc_library(
     name = "attribute_names_lib",
     srcs = [
diff --git a/src/istio/utils/logger.cc b/src/istio/utils/logger.cc
new file mode 100644
index 00000000000..f5b32c6981e
--- /dev/null
+++ b/src/istio/utils/logger.cc
@@ -0,0 +1,87 @@
+/* Copyright 2019 Istio Authors. All Rights Reserved.
+ *
+ * Licensed 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.
+ */
+
+#include "src/istio/utils/logger.h"
+#include <stdarg.h>
+
+namespace istio {
+namespace utils {
+
+Logger::~Logger() {}
+
+void Logger::log(Level level, const char *format, ...) {
+  if (!isLoggable(level)) {
+    return;
+  }
+
+  va_list args;
+  va_start(args, format);
+  char buffer[256];
+  ::vsnprintf(buffer, sizeof(buffer), format, args);
+  buffer[sizeof(buffer) - 1] = 0;
+  va_end(args);
+
+  writeBuffer(level, buffer);
+}
+
+// This is equivalent to the original mixer client logger, but is not used when
+// mixer client is used inside Envoy.  This preserves mixer client's
+// independence of the Envoy source code without forcing it to log (infrequenty)
+// to stdout.
+class DefaultLogger : public Logger {
+ protected:
+  virtual bool isLoggable(Level level) override {
+    switch (level) {
+      case Level::TRACE_:
+      case Level::DEBUG_:
+        return false;
+      case Level::INFO_:
+      case Level::WARN_:
+      case Level::ERROR_:
+        return true;
+    }
+  }
+
+  virtual void writeBuffer(Level level, const char *buffer) override {
+    fprintf(stderr, "%s %s\n", levelString(level), buffer);
+  }
+
+ private:
+  const char *levelString(Level level) {
+    switch (level) {
+      case Level::TRACE_:
+        return "TRACE";
+      case Level::DEBUG_:
+        return "DEBUG";
+      case Level::INFO_:
+        return "INFO";
+      case Level::WARN_:
+        return "WARN";
+      case Level::ERROR_:
+        return "ERROR";
+    }
+  }
+};
+
+static std::unique_ptr<Logger> active_logger{new DefaultLogger()};
+
+void setLogger(std::unique_ptr<Logger> logger) {
+  active_logger = std::move(logger);
+  MIXER_INFO("Logger active");
+}
+Logger &getLogger() { return *active_logger; }
+
+}  // namespace utils
+}  // namespace istio
diff --git a/src/istio/utils/logger.h b/src/istio/utils/logger.h
new file mode 100644
index 00000000000..b06baa30622
--- /dev/null
+++ b/src/istio/utils/logger.h
@@ -0,0 +1,107 @@
+/* Copyright 2019 Istio Authors. All Rights Reserved.
+ *
+ * Licensed 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.
+ */
+
+#pragma once
+
+#include <memory>
+
+namespace istio {
+namespace utils {
+
+class Logger {
+ public:
+  virtual ~Logger();
+
+  enum class Level { TRACE_, DEBUG_, INFO_, WARN_, ERROR_ };
+
+  void log(Level level, const char *format, ...);
+
+  virtual bool isLoggable(Level level) = 0;
+
+ protected:
+  virtual void writeBuffer(Level level, const char *buffer) = 0;
+};
+
+extern void setLogger(std::unique_ptr<Logger> logger);
+extern Logger &getLogger();
+
+}  // namespace utils
+}  // namespace istio
+
+#define STRINGLIT2(x) #x
+#define STRINGLIT(x) STRINGLIT2(x)
+#define FILE_LINE "[" __FILE__ ":" STRINGLIT(__LINE__) "] "
+
+#define MIXER_TRACE_ENABLED \
+  (istio::utils::getLogger().isLoggable(istio::utils::Logger::Level::TRACE_))
+#define MIXER_DEBUG_ENABLED \
+  (istio::utils::getLogger().isLoggable(istio::utils::Logger::Level::DEBUG_))
+#define MIXER_INFO_ENABLED \
+  (istio::utils::getLogger().isLoggable(istio::utils::Logger::Level::INFO_))
+#define MIXER_WARN_ENABLED \
+  (istio::utils::getLogger().isLoggable(istio::utils::Logger::Level::WARN_))
+#define MIXER_ERROR_ENABLED \
+  (istio::utils::getLogger().isLoggable(istio::utils::Logger::Level::ERROR_))
+
+#define MIXER_TRACE_INT(FORMAT, ...)                                 \
+  istio::utils::getLogger().log(istio::utils::Logger::Level::TRACE_, \
+                                FILE_LINE FORMAT, ##__VA_ARGS__)
+#define MIXER_DEBUG_INT(FORMAT, ...)                                 \
+  istio::utils::getLogger().log(istio::utils::Logger::Level::DEBUG_, \
+                                FILE_LINE FORMAT, ##__VA_ARGS__)
+#define MIXER_INFO_INT(FORMAT, ...)                                 \
+  istio::utils::getLogger().log(istio::utils::Logger::Level::INFO_, \
+                                FILE_LINE FORMAT, ##__VA_ARGS__)
+#define MIXER_WARN_INT(FORMAT, ...)                                 \
+  istio::utils::getLogger().log(istio::utils::Logger::Level::WARN_, \
+                                FILE_LINE FORMAT, ##__VA_ARGS__)
+#define MIXER_ERROR_INT(FORMAT, ...)                                 \
+  istio::utils::getLogger().log(istio::utils::Logger::Level::ERROR_, \
+                                FILE_LINE FORMAT, ##__VA_ARGS__)
+
+#define MIXER_TRACE(FORMAT, ...)              \
+  do {                                        \
+    if (MIXER_TRACE_ENABLED) {                \
+      MIXER_TRACE_INT(FORMAT, ##__VA_ARGS__); \
+    }                                         \
+  } while (0)
+
+#define MIXER_DEBUG(FORMAT, ...)              \
+  do {                                        \
+    if (MIXER_DEBUG_ENABLED) {                \
+      MIXER_DEBUG_INT(FORMAT, ##__VA_ARGS__); \
+    }                                         \
+  } while (0)
+
+#define MIXER_INFO(FORMAT, ...)              \
+  do {                                       \
+    if (MIXER_INFO_ENABLED) {                \
+      MIXER_INFO_INT(FORMAT, ##__VA_ARGS__); \
+    }                                        \
+  } while (0)
+
+#define MIXER_WARN(FORMAT, ...)              \
+  do {                                       \
+    if (MIXER_WARN_ENABLED) {                \
+      MIXER_WARN_INT(FORMAT, ##__VA_ARGS__); \
+    }                                        \
+  } while (0)
+
+#define MIXER_ERROR(FORMAT, ...)              \
+  do {                                        \
+    if (MIXER_ERROR_ENABLED) {                \
+      MIXER_ERROR_INT(FORMAT, ##__VA_ARGS__); \
+    }                                         \
+  } while (0)
diff --git a/src/istio/utils/logger_test.cc b/src/istio/utils/logger_test.cc
new file mode 100644
index 00000000000..a03522e578b
--- /dev/null
+++ b/src/istio/utils/logger_test.cc
@@ -0,0 +1,133 @@
+/* Copyright 2019 Istio Authors. All Rights Reserved.
+ *
+ * Licensed 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.
+ */
+
+#include "src/istio/utils/logger.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+
+namespace istio {
+namespace utils {
+
+class CountingArgument {
+ public:
+  const char* c_str() {
+    ++to_string_calls;
+    return "logged entity";
+  }
+
+  int to_string_calls{0};
+};
+
+class CountingLogger : public Logger {
+ public:
+  CountingLogger(int& is_loggable_calls, int& write_buffer_calls)
+      : is_loggable_calls_(is_loggable_calls),
+        write_buffer_calls_(write_buffer_calls) {}
+
+  virtual bool isLoggable(Level level) override {
+    ++is_loggable_calls_;
+
+    switch (level) {
+      case Level::TRACE_:
+      case Level::DEBUG_:
+        return false;
+      case Level::INFO_:
+      case Level::WARN_:
+      case Level::ERROR_:
+        return true;
+    }
+  }
+
+  virtual void writeBuffer(Level level, const char* buffer) override {
+    ++write_buffer_calls_;
+  }
+
+ private:
+  int& is_loggable_calls_;
+  int& write_buffer_calls_;
+};
+
+class LoggerTest : public ::testing::Test {
+ protected:
+  virtual void SetUp() override {
+    std::unique_ptr<Logger> logger{
+        new CountingLogger(is_loggable_calls_, write_buffer_calls_)};
+    setLogger(std::move(logger));
+    // Set logger itself logs something, so clear the counters
+    is_loggable_calls_ = 0;
+    write_buffer_calls_ = 0;
+  }
+
+  int is_loggable_calls_{0};
+  int write_buffer_calls_{0};
+};
+
+TEST_F(LoggerTest, CallArgsOnlyIfLoggable) {
+  CountingArgument entity;
+  int expected_to_string_calls = 0;
+  int expected_is_loggable_calls = 0;
+  int expected_write_buffer_calls = 0;
+
+  // TRACE and DEBUG shouldn't be logged and shouldn't have any affect on the
+  // arguments to be logged.
+
+  MIXER_TRACE("%s", entity.c_str());
+  ++expected_is_loggable_calls;
+
+  EXPECT_EQ(expected_to_string_calls, entity.to_string_calls);
+  EXPECT_EQ(expected_is_loggable_calls, is_loggable_calls_);
+  EXPECT_EQ(expected_write_buffer_calls, write_buffer_calls_);
+
+  MIXER_DEBUG("%s", entity.c_str());
+  ++expected_is_loggable_calls;
+
+  EXPECT_EQ(expected_to_string_calls, entity.to_string_calls);
+  EXPECT_EQ(expected_is_loggable_calls, is_loggable_calls_);
+  EXPECT_EQ(expected_write_buffer_calls, write_buffer_calls_);
+
+  // INFO+ will invoke their arguments once, be logged, and call isLoggable
+  // twice due to a redundant/defensive isLoggable check.
+
+  MIXER_INFO("%s", entity.c_str());
+  expected_is_loggable_calls += 2;
+  ++expected_to_string_calls;
+  ++expected_write_buffer_calls;
+
+  EXPECT_EQ(expected_to_string_calls, entity.to_string_calls);
+  EXPECT_EQ(expected_is_loggable_calls, is_loggable_calls_);
+  EXPECT_EQ(expected_write_buffer_calls, write_buffer_calls_);
+
+  MIXER_WARN("%s", entity.c_str());
+  expected_is_loggable_calls += 2;
+  ++expected_to_string_calls;
+  ++expected_write_buffer_calls;
+
+  EXPECT_EQ(expected_to_string_calls, entity.to_string_calls);
+  EXPECT_EQ(expected_is_loggable_calls, is_loggable_calls_);
+  EXPECT_EQ(expected_write_buffer_calls, write_buffer_calls_);
+
+  MIXER_ERROR("%s", entity.c_str());
+  expected_is_loggable_calls += 2;
+  ++expected_to_string_calls;
+  ++expected_write_buffer_calls;
+
+  EXPECT_EQ(expected_to_string_calls, entity.to_string_calls);
+  EXPECT_EQ(expected_is_loggable_calls, is_loggable_calls_);
+  EXPECT_EQ(expected_write_buffer_calls, write_buffer_calls_);
+}
+
+}  // namespace utils
+}  // namespace istio
