diff --git a/py_zipkin/encoding/_helpers.py b/py_zipkin/encoding/_helpers.py
index 4650d89..656891a 100644
--- a/py_zipkin/encoding/_helpers.py
+++ b/py_zipkin/encoding/_helpers.py
@@ -27,12 +27,6 @@ _V1Span = namedtuple(
 )
 
 
-_DROP_ANNOTATIONS_BY_KIND = {
-    Kind.CLIENT: {"ss", "sr"},
-    Kind.SERVER: {"cs", "cr"},
-}
-
-
 class Span(object):
     """Internal V2 Span representation."""
 
@@ -128,26 +122,22 @@ class Span(object):
         :return: newly generated _V1Span
         :rtype: _V1Span
         """
-        # We are simulating a full two-part span locally, so set cs=sr and ss=cr
-        full_annotations = OrderedDict(
-            [
-                ("cs", self.timestamp),
-                ("sr", self.timestamp),
-                ("ss", self.timestamp + self.duration),
-                ("cr", self.timestamp + self.duration),
-            ]
-        )
-
-        if self.kind != Kind.LOCAL:
-            # If kind is not LOCAL, then we only want client or
-            # server side annotations.
-            for ann in _DROP_ANNOTATIONS_BY_KIND[self.kind]:
-                del full_annotations[ann]
-
-        # Add user-defined annotations. We write them in full_annotations
+        annotations = OrderedDict([])
+        if self.kind == Kind.CLIENT:
+            annotations["cs"] = self.timestamp
+            annotations["cr"] = self.timestamp + self.duration
+        elif self.kind == Kind.SERVER:
+            annotations["sr"] = self.timestamp
+            annotations["ss"] = self.timestamp + self.duration
+        elif self.kind == Kind.PRODUCER:
+            annotations["ms"] = self.timestamp
+        elif self.kind == Kind.CONSUMER:
+            annotations["mr"] = self.timestamp
+
+        # Add user-defined annotations. We write them in annotations
         # instead of the opposite so that user annotations will override
         # any automatically generated annotation.
-        full_annotations.update(self.annotations)
+        annotations.update(self.annotations)
 
         return _V1Span(
             trace_id=self.trace_id,
@@ -157,7 +147,7 @@ class Span(object):
             timestamp=self.timestamp if self.shared is False else None,
             duration=self.duration if self.shared is False else None,
             endpoint=self.local_endpoint,
-            annotations=full_annotations,
+            annotations=annotations,
             binary_annotations=self.tags,
             remote_endpoint=self.remote_endpoint,
         )
diff --git a/py_zipkin/encoding/protobuf/zipkin.proto b/py_zipkin/encoding/protobuf/zipkin.proto
index a77dd69..105321f 100644
--- a/py_zipkin/encoding/protobuf/zipkin.proto
+++ b/py_zipkin/encoding/protobuf/zipkin.proto
@@ -1,16 +1,16 @@
-// Copyright 2018 The OpenZipkin Authors
 //
-// 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
+// Copyright 2018-2019 The OpenZipkin Authors
 //
-//     http://www.apache.org/licenses/LICENSE-2.0
+// 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.
 //
-// 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.
 
 syntax = "proto3";
 
@@ -212,3 +212,22 @@ message Annotation {
 message ListOfSpans {
   repeated Span spans = 1;
 }
+
+// Response for SpanService/Report RPC. This response currently does not return
+// any information beyond indicating that the request has finished. That said,
+// it may be extended in the future.
+message ReportResponse {
+}
+
+// SpanService allows reporting spans using gRPC, as opposed to HTTP POST
+// reporting. Implementations are asynchronous and may drop spans for reasons
+// of sampling or storage availability. While this is primarily used to store
+// spans, other operations may take place such as aggregation of service
+// dependencies or data cleaning.
+service SpanService {
+
+  // Report the provided spans to the collector. Analogous to the HTTP POST
+  // /api/v2/spans endpoint. Spans are not required to be complete or belonging
+  // to the same trace.
+  rpc Report(ListOfSpans) returns (ReportResponse) {}
+}
diff --git a/py_zipkin/encoding/protobuf/zipkin_pb2.py b/py_zipkin/encoding/protobuf/zipkin_pb2.py
index 449af7f..81733f8 100644
--- a/py_zipkin/encoding/protobuf/zipkin_pb2.py
+++ b/py_zipkin/encoding/protobuf/zipkin_pb2.py
@@ -1,9 +1,7 @@
+# -*- coding: utf-8 -*-
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: zipkin.proto
 
-import sys
-
-_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1"))
 from google.protobuf import descriptor as _descriptor
 from google.protobuf import message as _message
 from google.protobuf import reflection as _reflection
@@ -18,10 +16,9 @@ DESCRIPTOR = _descriptor.FileDescriptor(
     name="zipkin.proto",
     package="zipkin.proto3",
     syntax="proto3",
-    serialized_options=_b("\n\016zipkin2.proto3P\001"),
-    serialized_pb=_b(
-        '\n\x0czipkin.proto\x12\rzipkin.proto3"\xf5\x03\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x11\n\tparent_id\x18\x02 \x01(\x0c\x12\n\n\x02id\x18\x03 \x01(\x0c\x12&\n\x04kind\x18\x04 \x01(\x0e\x32\x18.zipkin.proto3.Span.Kind\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\x06\x12\x10\n\x08\x64uration\x18\x07 \x01(\x04\x12/\n\x0elocal_endpoint\x18\x08 \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12\x30\n\x0fremote_endpoint\x18\t \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12.\n\x0b\x61nnotations\x18\n \x03(\x0b\x32\x19.zipkin.proto3.Annotation\x12+\n\x04tags\x18\x0b \x03(\x0b\x32\x1d.zipkin.proto3.Span.TagsEntry\x12\r\n\x05\x64\x65\x62ug\x18\x0c \x01(\x08\x12\x0e\n\x06shared\x18\r \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"U\n\x04Kind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\x0c\n\x08PRODUCER\x10\x03\x12\x0c\n\x08\x43ONSUMER\x10\x04"J\n\x08\x45ndpoint\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12\x0c\n\x04ipv4\x18\x02 \x01(\x0c\x12\x0c\n\x04ipv6\x18\x03 \x01(\x0c\x12\x0c\n\x04port\x18\x04 \x01(\x05".\n\nAnnotation\x12\x11\n\ttimestamp\x18\x01 \x01(\x06\x12\r\n\x05value\x18\x02 \x01(\t"1\n\x0bListOfSpans\x12"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.zipkin.proto3.SpanB\x12\n\x0ezipkin2.proto3P\x01\x62\x06proto3'
-    ),
+    serialized_options=b"\n\016zipkin2.proto3P\001",
+    create_key=_descriptor._internal_create_key,
+    serialized_pb=b'\n\x0czipkin.proto\x12\rzipkin.proto3"\xf5\x03\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x11\n\tparent_id\x18\x02 \x01(\x0c\x12\n\n\x02id\x18\x03 \x01(\x0c\x12&\n\x04kind\x18\x04 \x01(\x0e\x32\x18.zipkin.proto3.Span.Kind\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\x06\x12\x10\n\x08\x64uration\x18\x07 \x01(\x04\x12/\n\x0elocal_endpoint\x18\x08 \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12\x30\n\x0fremote_endpoint\x18\t \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12.\n\x0b\x61nnotations\x18\n \x03(\x0b\x32\x19.zipkin.proto3.Annotation\x12+\n\x04tags\x18\x0b \x03(\x0b\x32\x1d.zipkin.proto3.Span.TagsEntry\x12\r\n\x05\x64\x65\x62ug\x18\x0c \x01(\x08\x12\x0e\n\x06shared\x18\r \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"U\n\x04Kind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\x0c\n\x08PRODUCER\x10\x03\x12\x0c\n\x08\x43ONSUMER\x10\x04"J\n\x08\x45ndpoint\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12\x0c\n\x04ipv4\x18\x02 \x01(\x0c\x12\x0c\n\x04ipv6\x18\x03 \x01(\x0c\x12\x0c\n\x04port\x18\x04 \x01(\x05".\n\nAnnotation\x12\x11\n\ttimestamp\x18\x01 \x01(\x06\x12\r\n\x05value\x18\x02 \x01(\t"1\n\x0bListOfSpans\x12"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.zipkin.proto3.Span"\x10\n\x0eReportResponse2T\n\x0bSpanService\x12\x45\n\x06Report\x12\x1a.zipkin.proto3.ListOfSpans\x1a\x1d.zipkin.proto3.ReportResponse"\x00\x42\x12\n\x0ezipkin2.proto3P\x01\x62\x06proto3',
 )
 
 
@@ -30,6 +27,7 @@ _SPAN_KIND = _descriptor.EnumDescriptor(
     full_name="zipkin.proto3.Span.Kind",
     filename=None,
     file=DESCRIPTOR,
+    create_key=_descriptor._internal_create_key,
     values=[
         _descriptor.EnumValueDescriptor(
             name="SPAN_KIND_UNSPECIFIED",
@@ -37,18 +35,39 @@ _SPAN_KIND = _descriptor.EnumDescriptor(
             number=0,
             serialized_options=None,
             type=None,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.EnumValueDescriptor(
-            name="CLIENT", index=1, number=1, serialized_options=None, type=None
+            name="CLIENT",
+            index=1,
+            number=1,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.EnumValueDescriptor(
-            name="SERVER", index=2, number=2, serialized_options=None, type=None
+            name="SERVER",
+            index=2,
+            number=2,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.EnumValueDescriptor(
-            name="PRODUCER", index=3, number=3, serialized_options=None, type=None
+            name="PRODUCER",
+            index=3,
+            number=3,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.EnumValueDescriptor(
-            name="CONSUMER", index=4, number=4, serialized_options=None, type=None
+            name="CONSUMER",
+            index=4,
+            number=4,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     containing_type=None,
@@ -65,6 +84,7 @@ _SPAN_TAGSENTRY = _descriptor.Descriptor(
     filename=None,
     file=DESCRIPTOR,
     containing_type=None,
+    create_key=_descriptor._internal_create_key,
     fields=[
         _descriptor.FieldDescriptor(
             name="key",
@@ -75,7 +95,7 @@ _SPAN_TAGSENTRY = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b("").decode("utf-8"),
+            default_value=b"".decode("utf-8"),
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -83,6 +103,7 @@ _SPAN_TAGSENTRY = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="value",
@@ -93,7 +114,7 @@ _SPAN_TAGSENTRY = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b("").decode("utf-8"),
+            default_value=b"".decode("utf-8"),
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -101,12 +122,13 @@ _SPAN_TAGSENTRY = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     extensions=[],
     nested_types=[],
     enum_types=[],
-    serialized_options=_b("8\001"),
+    serialized_options=b"8\001",
     is_extendable=False,
     syntax="proto3",
     extension_ranges=[],
@@ -121,6 +143,7 @@ _SPAN = _descriptor.Descriptor(
     filename=None,
     file=DESCRIPTOR,
     containing_type=None,
+    create_key=_descriptor._internal_create_key,
     fields=[
         _descriptor.FieldDescriptor(
             name="trace_id",
@@ -131,7 +154,7 @@ _SPAN = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b(""),
+            default_value=b"",
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -139,6 +162,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="parent_id",
@@ -149,7 +173,7 @@ _SPAN = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b(""),
+            default_value=b"",
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -157,6 +181,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="id",
@@ -167,7 +192,7 @@ _SPAN = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b(""),
+            default_value=b"",
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -175,6 +200,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="kind",
@@ -193,6 +219,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="name",
@@ -203,7 +230,7 @@ _SPAN = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b("").decode("utf-8"),
+            default_value=b"".decode("utf-8"),
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -211,6 +238,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="timestamp",
@@ -229,6 +257,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="duration",
@@ -247,6 +276,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="local_endpoint",
@@ -265,6 +295,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="remote_endpoint",
@@ -283,6 +314,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="annotations",
@@ -301,6 +333,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="tags",
@@ -319,6 +352,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="debug",
@@ -337,6 +371,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="shared",
@@ -355,6 +390,7 @@ _SPAN = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     extensions=[],
@@ -376,6 +412,7 @@ _ENDPOINT = _descriptor.Descriptor(
     filename=None,
     file=DESCRIPTOR,
     containing_type=None,
+    create_key=_descriptor._internal_create_key,
     fields=[
         _descriptor.FieldDescriptor(
             name="service_name",
@@ -386,7 +423,7 @@ _ENDPOINT = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b("").decode("utf-8"),
+            default_value=b"".decode("utf-8"),
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -394,6 +431,7 @@ _ENDPOINT = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="ipv4",
@@ -404,7 +442,7 @@ _ENDPOINT = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b(""),
+            default_value=b"",
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -412,6 +450,7 @@ _ENDPOINT = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="ipv6",
@@ -422,7 +461,7 @@ _ENDPOINT = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b(""),
+            default_value=b"",
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -430,6 +469,7 @@ _ENDPOINT = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="port",
@@ -448,6 +488,7 @@ _ENDPOINT = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     extensions=[],
@@ -469,6 +510,7 @@ _ANNOTATION = _descriptor.Descriptor(
     filename=None,
     file=DESCRIPTOR,
     containing_type=None,
+    create_key=_descriptor._internal_create_key,
     fields=[
         _descriptor.FieldDescriptor(
             name="timestamp",
@@ -487,6 +529,7 @@ _ANNOTATION = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
         _descriptor.FieldDescriptor(
             name="value",
@@ -497,7 +540,7 @@ _ANNOTATION = _descriptor.Descriptor(
             cpp_type=9,
             label=1,
             has_default_value=False,
-            default_value=_b("").decode("utf-8"),
+            default_value=b"".decode("utf-8"),
             message_type=None,
             enum_type=None,
             containing_type=None,
@@ -505,6 +548,7 @@ _ANNOTATION = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     extensions=[],
@@ -526,6 +570,7 @@ _LISTOFSPANS = _descriptor.Descriptor(
     filename=None,
     file=DESCRIPTOR,
     containing_type=None,
+    create_key=_descriptor._internal_create_key,
     fields=[
         _descriptor.FieldDescriptor(
             name="spans",
@@ -544,6 +589,7 @@ _LISTOFSPANS = _descriptor.Descriptor(
             extension_scope=None,
             serialized_options=None,
             file=DESCRIPTOR,
+            create_key=_descriptor._internal_create_key,
         ),
     ],
     extensions=[],
@@ -558,6 +604,27 @@ _LISTOFSPANS = _descriptor.Descriptor(
     serialized_end=708,
 )
 
+
+_REPORTRESPONSE = _descriptor.Descriptor(
+    name="ReportResponse",
+    full_name="zipkin.proto3.ReportResponse",
+    filename=None,
+    file=DESCRIPTOR,
+    containing_type=None,
+    create_key=_descriptor._internal_create_key,
+    fields=[],
+    extensions=[],
+    nested_types=[],
+    enum_types=[],
+    serialized_options=None,
+    is_extendable=False,
+    syntax="proto3",
+    extension_ranges=[],
+    oneofs=[],
+    serialized_start=710,
+    serialized_end=726,
+)
+
 _SPAN_TAGSENTRY.containing_type = _SPAN
 _SPAN.fields_by_name["kind"].enum_type = _SPAN_KIND
 _SPAN.fields_by_name["local_endpoint"].message_type = _ENDPOINT
@@ -570,25 +637,26 @@ DESCRIPTOR.message_types_by_name["Span"] = _SPAN
 DESCRIPTOR.message_types_by_name["Endpoint"] = _ENDPOINT
 DESCRIPTOR.message_types_by_name["Annotation"] = _ANNOTATION
 DESCRIPTOR.message_types_by_name["ListOfSpans"] = _LISTOFSPANS
+DESCRIPTOR.message_types_by_name["ReportResponse"] = _REPORTRESPONSE
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
 
 Span = _reflection.GeneratedProtocolMessageType(
     "Span",
     (_message.Message,),
-    dict(
-        TagsEntry=_reflection.GeneratedProtocolMessageType(
+    {
+        "TagsEntry": _reflection.GeneratedProtocolMessageType(
             "TagsEntry",
             (_message.Message,),
-            dict(
-                DESCRIPTOR=_SPAN_TAGSENTRY,
-                __module__="zipkin_pb2"
+            {
+                "DESCRIPTOR": _SPAN_TAGSENTRY,
+                "__module__": "zipkin_pb2"
                 # @@protoc_insertion_point(class_scope:zipkin.proto3.Span.TagsEntry)
-            ),
+            },
         ),
-        DESCRIPTOR=_SPAN,
-        __module__="zipkin_pb2"
+        "DESCRIPTOR": _SPAN,
+        "__module__": "zipkin_pb2"
         # @@protoc_insertion_point(class_scope:zipkin.proto3.Span)
-    ),
+    },
 )
 _sym_db.RegisterMessage(Span)
 _sym_db.RegisterMessage(Span.TagsEntry)
@@ -596,37 +664,75 @@ _sym_db.RegisterMessage(Span.TagsEntry)
 Endpoint = _reflection.GeneratedProtocolMessageType(
     "Endpoint",
     (_message.Message,),
-    dict(
-        DESCRIPTOR=_ENDPOINT,
-        __module__="zipkin_pb2"
+    {
+        "DESCRIPTOR": _ENDPOINT,
+        "__module__": "zipkin_pb2"
         # @@protoc_insertion_point(class_scope:zipkin.proto3.Endpoint)
-    ),
+    },
 )
 _sym_db.RegisterMessage(Endpoint)
 
 Annotation = _reflection.GeneratedProtocolMessageType(
     "Annotation",
     (_message.Message,),
-    dict(
-        DESCRIPTOR=_ANNOTATION,
-        __module__="zipkin_pb2"
+    {
+        "DESCRIPTOR": _ANNOTATION,
+        "__module__": "zipkin_pb2"
         # @@protoc_insertion_point(class_scope:zipkin.proto3.Annotation)
-    ),
+    },
 )
 _sym_db.RegisterMessage(Annotation)
 
 ListOfSpans = _reflection.GeneratedProtocolMessageType(
     "ListOfSpans",
     (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTOFSPANS,
-        __module__="zipkin_pb2"
+    {
+        "DESCRIPTOR": _LISTOFSPANS,
+        "__module__": "zipkin_pb2"
         # @@protoc_insertion_point(class_scope:zipkin.proto3.ListOfSpans)
-    ),
+    },
 )
 _sym_db.RegisterMessage(ListOfSpans)
 
+ReportResponse = _reflection.GeneratedProtocolMessageType(
+    "ReportResponse",
+    (_message.Message,),
+    {
+        "DESCRIPTOR": _REPORTRESPONSE,
+        "__module__": "zipkin_pb2"
+        # @@protoc_insertion_point(class_scope:zipkin.proto3.ReportResponse)
+    },
+)
+_sym_db.RegisterMessage(ReportResponse)
+
 
 DESCRIPTOR._options = None
 _SPAN_TAGSENTRY._options = None
+
+_SPANSERVICE = _descriptor.ServiceDescriptor(
+    name="SpanService",
+    full_name="zipkin.proto3.SpanService",
+    file=DESCRIPTOR,
+    index=0,
+    serialized_options=None,
+    create_key=_descriptor._internal_create_key,
+    serialized_start=728,
+    serialized_end=812,
+    methods=[
+        _descriptor.MethodDescriptor(
+            name="Report",
+            full_name="zipkin.proto3.SpanService.Report",
+            index=0,
+            containing_service=None,
+            input_type=_LISTOFSPANS,
+            output_type=_REPORTRESPONSE,
+            serialized_options=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+    ],
+)
+_sym_db.RegisterServiceDescriptor(_SPANSERVICE)
+
+DESCRIPTOR.services_by_name["SpanService"] = _SPANSERVICE
+
 # @@protoc_insertion_point(module_scope)
diff --git a/setup.py b/setup.py
index 4561051..024715b 100644
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,7 @@ setup(
     ],
     extras_require={
         ':python_version=="2.7"': ['enum34'],
-        'protobuf': 'protobuf',
+        'protobuf': 'protobuf >= 3.12.4',
     },
     classifiers=[
         "Development Status :: 3 - Alpha",

One-way tracing 
Hi, 
I am trying to enable tracing over rabbitmq messages. We have a few services that communicate over http and also send/receive events to rmq channels which then start further actions. The goal is to connect these requests and messages in the unified trace to better see causality behind certain actions.
My approach was to start a new span in publish_message / recieve_message methods and store/read zipkin headers from message headers. Example span when publishing: 

```
with zipkin_span( service_name='some name', span_name='some name', kind=Kind.PRODUCER ):
     headers = create_http_headers_for_new_span()

```
Receiving message is similar but there I put kind=Kind.CONSUMER.
I have a problem with kind option here is the exact error:

`ERROR: Error emitting zipkin trace. ZipkinError("(logging_helper.py:110) KeyError: <Kind.PRODUCER: 'PRODUCER'>") [py_zipkin.zipkin:500]`

don't understand why this is happening, are producer/consumer spans supported in version 0.20.0 ?
diff --git a/tests/encoding/_helpers_test.py b/tests/encoding/_helpers_test.py
index 81eaf20..4ed0385 100644
--- a/tests/encoding/_helpers_test.py
+++ b/tests/encoding/_helpers_test.py
@@ -1,4 +1,5 @@
 import socket
+from collections import OrderedDict
 
 import mock
 import pytest
@@ -106,3 +107,30 @@ def test_malformed_host():
     assert endpoint.port == 8080
     assert endpoint.ipv4 is None
     assert endpoint.ipv6 is None
+
+
+class TestSpan(object):
+    @pytest.mark.parametrize(
+        ["kind", "annotations"],
+        [
+            (Kind.CLIENT, OrderedDict([("cs", 26.0), ("cr", 30.0)])),
+            (Kind.SERVER, OrderedDict([("sr", 26.0), ("ss", 30.0)])),
+            (Kind.PRODUCER, OrderedDict([("ms", 26.0)])),
+            (Kind.CONSUMER, OrderedDict([("mr", 26.0)])),
+        ],
+    )
+    def test_v1_span(self, kind, annotations):
+        endpoint = create_endpoint(port=8080, service_name="foo", host="80")
+        span = Span(
+            trace_id=generate_random_64bit_string(),
+            name="test span",
+            parent_id=generate_random_64bit_string(),
+            span_id=generate_random_64bit_string(),
+            kind=kind,
+            timestamp=26.0,
+            duration=4.0,
+            local_endpoint=endpoint,
+        )
+        v1_span = span.build_v1_span()
+
+        assert v1_span.annotations == annotations
diff --git a/tests/integration/encoding_test.py b/tests/integration/encoding_test.py
index 8942662..0c066e1 100644
--- a/tests/integration/encoding_test.py
+++ b/tests/integration/encoding_test.py
@@ -36,7 +36,7 @@ def us(seconds):
 
 
 def check_v1_json(obj, zipkin_attrs, inner_span_id, ts):
-    inner_span, root_span = json.loads(obj)
+    inner_span, producer_span, root_span = json.loads(obj)
 
     endpoint = {
         "ipv4": "10.0.0.0",
@@ -74,18 +74,23 @@ def check_v1_json(obj, zipkin_attrs, inner_span_id, ts):
         "timestamp": us(ts),
         "duration": us(5),
         "binaryAnnotations": [],
-        "annotations": [
-            {"endpoint": endpoint, "timestamp": us(ts), "value": "cs"},
-            {"endpoint": endpoint, "timestamp": us(ts), "value": "sr"},
-            {"endpoint": endpoint, "timestamp": us(ts + 5), "value": "ss"},
-            {"endpoint": endpoint, "timestamp": us(ts + 5), "value": "cr"},
-            {"endpoint": endpoint, "timestamp": us(ts), "value": "ws"},
-        ],
+        "annotations": [{"endpoint": endpoint, "timestamp": us(ts), "value": "ws"}],
+    }
+
+    assert producer_span == {
+        "traceId": zipkin_attrs.trace_id,
+        "parentId": zipkin_attrs.span_id,
+        "name": "producer_span",
+        "id": inner_span_id,
+        "timestamp": us(ts),
+        "duration": us(10),
+        "binaryAnnotations": [],
+        "annotations": [{"endpoint": endpoint, "timestamp": us(ts), "value": "ms"}],
     }
 
 
 def check_v1_thrift(obj, zipkin_attrs, inner_span_id, ts):
-    inner_span, root_span = _decode_binary_thrift_objs(obj)
+    inner_span, producer_span, root_span = _decode_binary_thrift_objs(obj)
 
     endpoint = thrift.create_endpoint(
         port=8080, service_name="test_service_name", ipv4="10.0.0.0",
@@ -131,10 +136,7 @@ def check_v1_thrift(obj, zipkin_attrs, inner_span_id, ts):
         trace_id=zipkin_attrs.trace_id,
         span_name="inner_span",
         annotations=thrift.annotation_list_builder(
-            OrderedDict(
-                [("cs", ts), ("sr", ts), ("ss", ts + 5), ("cr", ts + 5), ("ws", ts)]
-            ),
-            endpoint,
+            OrderedDict([("ws", ts)]), endpoint,
         ),
         binary_annotations=[],
         timestamp_s=ts,
@@ -147,9 +149,28 @@ def check_v1_thrift(obj, zipkin_attrs, inner_span_id, ts):
     print(expected_inner)
     assert inner_span == expected_inner
 
+    expected_producer = thrift.create_span(
+        span_id=inner_span_id,
+        parent_span_id=zipkin_attrs.span_id,
+        trace_id=zipkin_attrs.trace_id,
+        span_name="producer_span",
+        annotations=thrift.annotation_list_builder(
+            OrderedDict([("ms", ts)]), endpoint,
+        ),
+        binary_annotations=[],
+        timestamp_s=ts,
+        duration_s=10,
+    )
+    # py.test diffs of thrift Spans are pretty useless and hide many things
+    # These prints would only appear on stdout if the test fails and help comparing
+    # the 2 spans.
+    print(producer_span)
+    print(expected_producer)
+    assert producer_span == expected_producer
+
 
 def check_v2_json(obj, zipkin_attrs, inner_span_id, ts):
-    inner_span, root_span = json.loads(obj)
+    inner_span, producer_span, root_span = json.loads(obj)
 
     assert root_span == {
         "traceId": zipkin_attrs.trace_id,
@@ -188,6 +209,21 @@ def check_v2_json(obj, zipkin_attrs, inner_span_id, ts):
         "annotations": [{"timestamp": us(ts), "value": "ws"}],
     }
 
+    assert producer_span == {
+        "traceId": zipkin_attrs.trace_id,
+        "name": "producer_span",
+        "parentId": zipkin_attrs.span_id,
+        "id": inner_span_id,
+        "kind": "PRODUCER",
+        "timestamp": us(ts),
+        "duration": us(10),
+        "localEndpoint": {
+            "ipv4": "10.0.0.0",
+            "port": 8080,
+            "serviceName": "test_service_name",
+        },
+    }
+
 
 @pytest.mark.parametrize(
     "encoding,validate_fn",
@@ -217,7 +253,9 @@ def test_encoding(encoding, validate_fn):
         # zipkin.py start, logging_helper.start, 3 x logging_helper.stop
         # I don't understand why logging_helper.stop would run 3 times, but
         # that's what I'm seeing in the test
-        mock_time.side_effect = iter([ts, ts, ts + 10, ts + 10, ts + 10])
+        mock_time.side_effect = iter(
+            [ts, ts, ts + 10, ts + 10, ts + 10, ts + 10, ts + 10]
+        )
         with zipkin.zipkin_span(
             service_name="test_service_name",
             span_name="test_span_name",
@@ -242,6 +280,14 @@ def test_encoding(encoding, validate_fn):
                     span.add_sa_binary_annotation(
                         8888, "sa_service", "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
                     )
+                with zipkin.zipkin_span(
+                    service_name="test_service_name",
+                    span_name="producer_span",
+                    timestamp=ts,
+                    duration=10,
+                    kind=Kind.PRODUCER,
+                ):
+                    pass
 
     output = mock_transport_handler.get_payloads()[0]
     validate_fn(output, zipkin_attrs, inner_span_id, ts)
diff --git a/tests/integration/zipkin_integration_test.py b/tests/integration/zipkin_integration_test.py
index 0bc0034..128a000 100644
--- a/tests/integration/zipkin_integration_test.py
+++ b/tests/integration/zipkin_integration_test.py
@@ -144,6 +144,7 @@ def test_annotation_override():
         with zipkin.zipkin_span(
             service_name="nested_service",
             span_name="nested_span",
+            kind=Kind.CLIENT,
             annotations={"nested_annotation": 43, "cs": 100, "cr": 300},
             binary_annotations={"nested_key": "nested_value"},
         ):
@@ -162,9 +163,9 @@ def test_annotation_override():
         # Local nested spans report timestamp and duration
         assert nested_span["timestamp"] is not None
         assert nested_span["duration"] is not None
-        assert len(nested_span["annotations"]) == 5
+        assert len(nested_span["annotations"]) == 3
         assert sorted([ann["value"] for ann in nested_span["annotations"]]) == sorted(
-            ["ss", "sr", "cs", "cr", "nested_annotation"]
+            ["cs", "cr", "nested_annotation"]
         )
         for ann in nested_span["annotations"]:
             if ann["value"] == "nested_annotation":
@@ -533,13 +534,7 @@ def test_include_still_works(encoding):
             "sr",
             "ss",
         ]
-        assert len(local_span["annotations"]) == 4
-        assert sorted([ann["value"] for ann in local_span["annotations"]]) == [
-            "cr",
-            "cs",
-            "sr",
-            "ss",
-        ]
+        assert len(local_span["annotations"]) == 0
     elif encoding == Encoding.V2_JSON:
         assert client_span["kind"] == "CLIENT"
         assert server_span["kind"] == "SERVER"

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest numpy>=1.16.0 pandas>=1.0.0
apt-get update apt-get install -y gcc
3.9
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==4.5.4
Cython==3.0.12
exceptiongroup==1.2.2
importlib_metadata==8.6.1
iniconfig==2.1.0
mock==3.0.5
numpy==2.0.2
ordereddict==1.1
packaging==24.2
pandas==2.2.3
pluggy==1.5.0
ply==3.11
py-cpuinfo==9.0.0
-e git+https://github.com/Yelp/py_zipkin.git@8e733506c399967ea74c56b99a9a421e1bb1736a#egg=py_zipkin
pygal==3.0.5
pygaljs==1.0.2
pytest==8.3.5
pytest-benchmark==5.1.0
python-dateutil==2.9.0.post0
pytz==2025.2
six==1.17.0
thriftpy2==0.5.2
tomli==2.2.1
tzdata==2025.2
zipp==3.21.0

name: py_zipkin
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==4.5.4
      - cython==3.0.12
      - exceptiongroup==1.2.2
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - mock==3.0.5
      - numpy==2.0.2
      - ordereddict==1.1
      - packaging==24.2
      - pandas==2.2.3
      - pluggy==1.5.0
      - ply==3.11
      - py-cpuinfo==9.0.0
      - pygal==3.0.5
      - pygaljs==1.0.2
      - pytest==8.3.5
      - pytest-benchmark==5.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - six==1.17.0
      - thriftpy2==0.5.2
      - tomli==2.2.1
      - tzdata==2025.2
      - zipp==3.21.0
prefix: /opt/conda/envs/py_zipkin

tests/encoding/_helpers_test.py::TestSpan::test_v1_span[Kind.PRODUCER-annotations2] tests/encoding/_helpers_test.py::TestSpan::test_v1_span[Kind.CONSUMER-annotations3] tests/integration/encoding_test.py::test_encoding[Encoding.V1_JSON-check_v1_json] tests/integration/zipkin_integration_test.py::test_include_still_works[Encoding.V1_JSON]
tests/integration/encoding_test.py::test_encoding[Encoding.V1_THRIFT-check_v1_thrift]
tests/encoding/_helpers_test.py::test_create_span_with_bad_kind tests/encoding/_helpers_test.py::test_create_span_with_bad_local_endpoint tests/encoding/_helpers_test.py::test_create_span_with_bad_remote_endpoint tests/encoding/_helpers_test.py::test_create_endpoint_defaults_service_name tests/encoding/_helpers_test.py::test_create_endpoint_correct_host_ip tests/encoding/_helpers_test.py::test_create_endpoint_defaults_localhost tests/encoding/_helpers_test.py::test_create_endpoint_ipv6 tests/encoding/_helpers_test.py::test_malformed_host tests/encoding/_helpers_test.py::TestSpan::test_v1_span[Kind.CLIENT-annotations0] tests/encoding/_helpers_test.py::TestSpan::test_v1_span[Kind.SERVER-annotations1] tests/integration/encoding_test.py::test_encoding[Encoding.V2_JSON-check_v2_json] tests/integration/zipkin_integration_test.py::test_starting_zipkin_trace_with_sampling_rate tests/integration/zipkin_integration_test.py::test_starting_zipkin_trace_with_128bit_trace_id tests/integration/zipkin_integration_test.py::test_span_inside_trace tests/integration/zipkin_integration_test.py::test_annotation_override tests/integration/zipkin_integration_test.py::test_sr_ss_annotation_override[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_sr_ss_annotation_override[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_service_span[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_service_span[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_service_exits_on_erroneous_span tests/integration/zipkin_integration_test.py::test_service_span_report_timestamp_override tests/integration/zipkin_integration_test.py::test_service_span_that_is_independently_sampled[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_service_span_that_is_independently_sampled[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_zipkin_trace_with_no_sampling_no_firehose tests/integration/zipkin_integration_test.py::test_zipkin_trace_with_no_sampling_with_firehose tests/integration/zipkin_integration_test.py::test_no_sampling_with_inner_span tests/integration/zipkin_integration_test.py::test_client_span[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_client_span[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_server_span[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_server_span[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_include_still_works[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_can_set_sa_annotation[Encoding.V1_JSON] tests/integration/zipkin_integration_test.py::test_can_set_sa_annotation[Encoding.V2_JSON] tests/integration/zipkin_integration_test.py::test_memory_leak
py_zipkin/encoding/protobuf/zipkin.proto py_zipkin/encoding/_helpers.py py_zipkin/encoding/protobuf/zipkin_pb2.py setup.py
py_zipkin/encoding/protobuf/zipkin.proto py_zipkin/encoding/_helpers.py py_zipkin/encoding/protobuf/zipkin_pb2.py setup.py