1// Copyright 2017, OpenCensus Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package stackdriver
16
17import (
18	"fmt"
19	"math"
20	"strconv"
21	"time"
22	"unicode/utf8"
23
24	timestamppb "github.com/golang/protobuf/ptypes/timestamp"
25	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
26	"go.opencensus.io/plugin/ochttp"
27	"go.opencensus.io/trace"
28	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
29	tracepb "google.golang.org/genproto/googleapis/devtools/cloudtrace/v2"
30	statuspb "google.golang.org/genproto/googleapis/rpc/status"
31)
32
33const (
34	maxAnnotationEventsPerSpan = 32
35	maxMessageEventsPerSpan    = 128
36	maxAttributeStringValue    = 256
37	agentLabel                 = "g.co/agent"
38
39	labelHTTPHost       = `/http/host`
40	labelHTTPMethod     = `/http/method`
41	labelHTTPStatusCode = `/http/status_code`
42	labelHTTPPath       = `/http/path`
43	labelHTTPUserAgent  = `/http/user_agent`
44)
45
46// proto returns a protocol buffer representation of a SpanData.
47func protoFromSpanData(s *trace.SpanData, projectID string, mr *monitoredrespb.MonitoredResource) *tracepb.Span {
48	if s == nil {
49		return nil
50	}
51
52	traceIDString := s.SpanContext.TraceID.String()
53	spanIDString := s.SpanContext.SpanID.String()
54
55	name := s.Name
56	switch s.SpanKind {
57	case trace.SpanKindClient:
58		name = "Sent." + name
59	case trace.SpanKindServer:
60		name = "Recv." + name
61	}
62
63	sp := &tracepb.Span{
64		Name:                    "projects/" + projectID + "/traces/" + traceIDString + "/spans/" + spanIDString,
65		SpanId:                  spanIDString,
66		DisplayName:             trunc(name, 128),
67		StartTime:               timestampProto(s.StartTime),
68		EndTime:                 timestampProto(s.EndTime),
69		SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: !s.HasRemoteParent},
70	}
71	if p := s.ParentSpanID; p != (trace.SpanID{}) {
72		sp.ParentSpanId = p.String()
73	}
74	if s.Status.Code != 0 || s.Status.Message != "" {
75		sp.Status = &statuspb.Status{Code: s.Status.Code, Message: s.Status.Message}
76	}
77
78	var annotations, droppedAnnotationsCount, messageEvents, droppedMessageEventsCount int
79	copyAttributes(&sp.Attributes, s.Attributes)
80
81	// Copy MonitoredResources as span Attributes
82	sp.Attributes = copyMonitoredResourceAttributes(sp.Attributes, mr)
83
84	as := s.Annotations
85	for i, a := range as {
86		if annotations >= maxAnnotationEventsPerSpan {
87			droppedAnnotationsCount = len(as) - i
88			break
89		}
90		annotation := &tracepb.Span_TimeEvent_Annotation{Description: trunc(a.Message, maxAttributeStringValue)}
91		copyAttributes(&annotation.Attributes, a.Attributes)
92		event := &tracepb.Span_TimeEvent{
93			Time:  timestampProto(a.Time),
94			Value: &tracepb.Span_TimeEvent_Annotation_{Annotation: annotation},
95		}
96		annotations++
97		if sp.TimeEvents == nil {
98			sp.TimeEvents = &tracepb.Span_TimeEvents{}
99		}
100		sp.TimeEvents.TimeEvent = append(sp.TimeEvents.TimeEvent, event)
101	}
102
103	if sp.Attributes == nil {
104		sp.Attributes = &tracepb.Span_Attributes{
105			AttributeMap: make(map[string]*tracepb.AttributeValue),
106		}
107	}
108
109	// Only set the agent label if it is not already set. That enables the
110	// OpenCensus agent/collector to set the agent label based on the library that
111	// sent the span to the agent.
112	if _, hasAgent := sp.Attributes.AttributeMap[agentLabel]; !hasAgent {
113		sp.Attributes.AttributeMap[agentLabel] = &tracepb.AttributeValue{
114			Value: &tracepb.AttributeValue_StringValue{
115				StringValue: trunc(userAgent, maxAttributeStringValue),
116			},
117		}
118	}
119
120	es := s.MessageEvents
121	for i, e := range es {
122		if messageEvents >= maxMessageEventsPerSpan {
123			droppedMessageEventsCount = len(es) - i
124			break
125		}
126		messageEvents++
127		if sp.TimeEvents == nil {
128			sp.TimeEvents = &tracepb.Span_TimeEvents{}
129		}
130		sp.TimeEvents.TimeEvent = append(sp.TimeEvents.TimeEvent, &tracepb.Span_TimeEvent{
131			Time: timestampProto(e.Time),
132			Value: &tracepb.Span_TimeEvent_MessageEvent_{
133				MessageEvent: &tracepb.Span_TimeEvent_MessageEvent{
134					Type:                  tracepb.Span_TimeEvent_MessageEvent_Type(e.EventType),
135					Id:                    e.MessageID,
136					UncompressedSizeBytes: e.UncompressedByteSize,
137					CompressedSizeBytes:   e.CompressedByteSize,
138				},
139			},
140		})
141	}
142
143	if droppedAnnotationsCount != 0 || droppedMessageEventsCount != 0 {
144		if sp.TimeEvents == nil {
145			sp.TimeEvents = &tracepb.Span_TimeEvents{}
146		}
147		sp.TimeEvents.DroppedAnnotationsCount = clip32(droppedAnnotationsCount)
148		sp.TimeEvents.DroppedMessageEventsCount = clip32(droppedMessageEventsCount)
149	}
150
151	if len(s.Links) > 0 {
152		sp.Links = &tracepb.Span_Links{}
153		sp.Links.Link = make([]*tracepb.Span_Link, 0, len(s.Links))
154		for _, l := range s.Links {
155			link := &tracepb.Span_Link{
156				TraceId: l.TraceID.String(),
157				SpanId:  l.SpanID.String(),
158				Type:    tracepb.Span_Link_Type(l.Type),
159			}
160			copyAttributes(&link.Attributes, l.Attributes)
161			sp.Links.Link = append(sp.Links.Link, link)
162		}
163	}
164	return sp
165}
166
167// timestampProto creates a timestamp proto for a time.Time.
168func timestampProto(t time.Time) *timestamppb.Timestamp {
169	return &timestamppb.Timestamp{
170		Seconds: t.Unix(),
171		Nanos:   int32(t.Nanosecond()),
172	}
173}
174
175// copyMonitoredResourceAttributes copies proto monitoredResource to proto map field (Span_Attributes)
176// it creates the map if it is nil.
177func copyMonitoredResourceAttributes(out *tracepb.Span_Attributes, mr *monitoredrespb.MonitoredResource) *tracepb.Span_Attributes {
178	if mr == nil {
179		return out
180	}
181	if out == nil {
182		out = &tracepb.Span_Attributes{}
183	}
184	if out.AttributeMap == nil {
185		out.AttributeMap = make(map[string]*tracepb.AttributeValue)
186	}
187	for k, v := range mr.Labels {
188		av := attributeValue(v)
189		out.AttributeMap[fmt.Sprintf("g.co/r/%s/%s", mr.Type, k)] = av
190	}
191	return out
192}
193
194// copyAttributes copies a map of attributes to a proto map field.
195// It creates the map if it is nil.
196func copyAttributes(out **tracepb.Span_Attributes, in map[string]interface{}) {
197	if len(in) == 0 {
198		return
199	}
200	if *out == nil {
201		*out = &tracepb.Span_Attributes{}
202	}
203	if (*out).AttributeMap == nil {
204		(*out).AttributeMap = make(map[string]*tracepb.AttributeValue)
205	}
206	var dropped int32
207	for key, value := range in {
208		av := attributeValue(value)
209		if av == nil {
210			continue
211		}
212		switch key {
213		case ochttp.PathAttribute:
214			(*out).AttributeMap[labelHTTPPath] = av
215		case ochttp.HostAttribute:
216			(*out).AttributeMap[labelHTTPHost] = av
217		case ochttp.MethodAttribute:
218			(*out).AttributeMap[labelHTTPMethod] = av
219		case ochttp.UserAgentAttribute:
220			(*out).AttributeMap[labelHTTPUserAgent] = av
221		case ochttp.StatusCodeAttribute:
222			(*out).AttributeMap[labelHTTPStatusCode] = av
223		default:
224			if len(key) > 128 {
225				dropped++
226				continue
227			}
228			(*out).AttributeMap[key] = av
229		}
230	}
231	(*out).DroppedAttributesCount = dropped
232}
233
234func attributeValue(v interface{}) *tracepb.AttributeValue {
235	switch value := v.(type) {
236	case bool:
237		return &tracepb.AttributeValue{
238			Value: &tracepb.AttributeValue_BoolValue{BoolValue: value},
239		}
240	case int64:
241		return &tracepb.AttributeValue{
242			Value: &tracepb.AttributeValue_IntValue{IntValue: value},
243		}
244	case float64:
245		// TODO: set double value if Stackdriver Trace support it in the future.
246		return &tracepb.AttributeValue{
247			Value: &tracepb.AttributeValue_StringValue{
248				StringValue: trunc(strconv.FormatFloat(value, 'f', -1, 64),
249					maxAttributeStringValue)},
250		}
251	case string:
252		return &tracepb.AttributeValue{
253			Value: &tracepb.AttributeValue_StringValue{StringValue: trunc(value, maxAttributeStringValue)},
254		}
255	}
256	return nil
257}
258
259// trunc returns a TruncatableString truncated to the given limit.
260func trunc(s string, limit int) *tracepb.TruncatableString {
261	if len(s) > limit {
262		b := []byte(s[:limit])
263		for {
264			r, size := utf8.DecodeLastRune(b)
265			if r == utf8.RuneError && size == 1 {
266				b = b[:len(b)-1]
267			} else {
268				break
269			}
270		}
271		return &tracepb.TruncatableString{
272			Value:              string(b),
273			TruncatedByteCount: clip32(len(s) - len(b)),
274		}
275	}
276	return &tracepb.TruncatableString{
277		Value:              s,
278		TruncatedByteCount: 0,
279	}
280}
281
282// clip32 clips an int to the range of an int32.
283func clip32(x int) int32 {
284	if x < math.MinInt32 {
285		return math.MinInt32
286	}
287	if x > math.MaxInt32 {
288		return math.MaxInt32
289	}
290	return int32(x)
291}
292