[TimeZonePolicy] Add OTHER_TIME_ZONE time zone policy

Adds a new option for a metric's TimeZonePolicy, in
addition to UTC or device-local time. If the new
option is selected, a valid IANA time zone identifier
(https://www.iana.org/time-zones) should be specified
using the new `other_time_zone` field of
MetricDefinition.

The new option will be implemented in follow-ups.

Bug: 92131
Change-Id: Ieef6ca6c795ff2fbcedad433aa6ed1362d95b8d9
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/611001
Reviewed-by: Zach Bush <zmbush@google.com>
Reviewed-by: Cameron Dale <camrdale@google.com>
Commit-Queue: Laura Peskin <pesk@google.com>
diff --git a/src/bin/config_parser/src/config_validator/metric_definitions.go b/src/bin/config_parser/src/config_validator/metric_definitions.go
index dfabe88..f6e9838 100644
--- a/src/bin/config_parser/src/config_validator/metric_definitions.go
+++ b/src/bin/config_parser/src/config_validator/metric_definitions.go
@@ -100,6 +100,10 @@
 		validationErrors.addError("metric_units", err)
 	}
 
+	if err := validateMetricTimeZone(m); err != nil {
+		validationErrors.addError("time_zone_policy", err)
+	}
+
 	if m.IntBuckets != nil && m.MetricType != config.MetricDefinition_INT_HISTOGRAM && m.MetricType != config.MetricDefinition_INTEGER_HISTOGRAM {
 		validationErrors.addError("int_buckets", fmt.Errorf("int_buckets can only be set for metrics for metric type INT_HISTOGRAM and INTEGER_HISTOGRAM"))
 	}
@@ -340,6 +344,23 @@
 	return nil
 }
 
+func validateMetricTimeZone(m config.MetricDefinition) error {
+	if m.TimeZonePolicy == config.MetricDefinition_OTHER_TIME_ZONE {
+		if m.OtherTimeZone == "" {
+			return fmt.Errorf("metrics with TimeZonePolicy OTHER_TIME_ZONE must specify other_time_zone")
+		}
+		_, err := time.LoadLocation(m.OtherTimeZone)
+		if err != nil {
+			return fmt.Errorf("other_time_zone is not a valid IANA time zone identifier (see https://iana.org/time-zones)")
+		}
+	} else {
+		if m.OtherTimeZone != "" {
+			return fmt.Errorf("only metrics with TimeZonePolicy OTHER_TIME_ZONE should specify other_time_zone")
+		}
+	}
+	return nil
+}
+
 ///////////////////////////////////////////////////////////////
 // Validation for specific metric types:
 ///////////////////////////////////////////////////////////////
diff --git a/src/bin/config_parser/src/config_validator/metric_definitions_test.go b/src/bin/config_parser/src/config_validator/metric_definitions_test.go
index d3266d9..2ffcd3f 100644
--- a/src/bin/config_parser/src/config_validator/metric_definitions_test.go
+++ b/src/bin/config_parser/src/config_validator/metric_definitions_test.go
@@ -418,6 +418,50 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+// Tests concerning metric time zone policy.
+////////////////////////////////////////////////////////////////////////////////
+
+func TestValidateTimeZonePolicyOtherTimeZoneNotRequired(t *testing.T) {
+	m := makeValidMetric(config.MetricDefinition_OCCURRENCE)
+	m.TimeZonePolicy = config.MetricDefinition_UTC
+	if err := validateMetricDefinition(m); err != nil {
+		t.Error("Rejected metric with valid UTC TimeZonePolicy")
+	}
+	m.OtherTimeZone = "America/Los_Angeles"
+	if err := validateMetricDefinition(m); err == nil {
+		t.Error("Accepted metric with UTC TimeZonePolicy and with other_time_zone set")
+	}
+
+	m.TimeZonePolicy = config.MetricDefinition_LOCAL
+	m.OtherTimeZone = ""
+	if err := validateMetricDefinition(m); err != nil {
+		t.Error("Rejected metric with valid LOCAL TimeZonePolicy")
+	}
+	m.OtherTimeZone = "America/Los_Angeles"
+	if err := validateMetricDefinition(m); err == nil {
+		t.Error("Accepted metric with LOCAL TimeZonePolicy and with other_time_zone set")
+	}
+}
+
+func TestValidateTimeZonePolicyOtherTimeZoneRequired(t *testing.T) {
+	m := makeValidMetric(config.MetricDefinition_OCCURRENCE)
+	m.TimeZonePolicy = config.MetricDefinition_OTHER_TIME_ZONE
+	if err := validateMetricDefinition(m); err == nil {
+		t.Error("Accepted metric with OTHER_TIME_ZONE TimeZonePolicy but no other_time_zone set")
+	}
+
+	m.OtherTimeZone = "PST"
+	if err := validateMetricDefinition(m); err == nil {
+		t.Error("Accepted metric with OTHER_TIME_ZONE TimeZone and invalid time zone identifier as other_time_zone")
+	}
+
+	m.OtherTimeZone = "America/Los_Angeles"
+	if err := validateMetricDefinition(m); err != nil {
+		t.Error("Rejected metric with OTHER_TIME_ZONE TimeZonePolicy and valid other_time_zone time zone identifier")
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // Tests concerning INT_HISTOGRAM, INTEGER and INTEGER_HISTOGRAM metrics.
 ////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/registry/metric_definition.proto b/src/registry/metric_definition.proto
index 890d430..458a724 100644
--- a/src/registry/metric_definition.proto
+++ b/src/registry/metric_definition.proto
@@ -30,7 +30,7 @@
 //
 // A MetricDefinition is used to define a Metric.
 //
-// Next ID: 25
+// Next ID: 26
 message MetricDefinition {
   reserved 6, 7, 9, 17;
   reserved "event_codes", "max_event_code", "parts", "event_code_buffer_max";
@@ -391,19 +391,27 @@
 
   /////////// The rest of the fields are used with all Metric types ///////////
 
-  // A TimeZonePolicy specifies how the day_index of an Event should
-  // be computed based on the actual time of logging.
+  // A TimeZonePolicy specifies how the day index of an Event should be computed
+  // based on the system time when the Event is logged.
   enum TimeZonePolicy {
-    // Use the date in UTC at logging time to compute the day_index.
+    // Use the date in UTC at logging time to compute the day index.
     UTC = 0;
 
-    // Use the local date at logging time to compute the day_index.
+    // Use the device-local date at logging time to compute the day index.
     LOCAL = 1;
+
+    // Use the time zone specified in the `other_time_zone` field to compute the
+    // day index.
+    OTHER_TIME_ZONE = 2;
   }
 
   // The TimeZonePolicy for this Metric (Optional. Defaults to UTC)
   TimeZonePolicy time_zone_policy = 10;
 
+  // An IANA time zone identifier (https://iana.org/time-zones). Should be set if
+  // and only if the metric's `time_zone_policy` is OTHER_TIME_ZONE.
+  string other_time_zone = 25;
+
   message Metadata {
     // The date after which this metric is no longer valid. If this field is not
     // supplied, the metric is considered currently expired, and is not