1// Copyright 2016 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package opentsdb 15 16import ( 17 "bytes" 18 "fmt" 19 20 "github.com/prometheus/common/model" 21) 22 23// TagValue is a model.LabelValue that implements json.Marshaler and 24// json.Unmarshaler. These implementations avoid characters illegal in 25// OpenTSDB. See the MarshalJSON for details. TagValue is used for the values of 26// OpenTSDB tags as well as for OpenTSDB metric names. 27type TagValue model.LabelValue 28 29// MarshalJSON marshals this TagValue into JSON that only contains runes allowed 30// in OpenTSDB. It implements json.Marshaler. The runes allowed in OpenTSDB are 31// all single-byte. This function encodes the arbitrary byte sequence found in 32// this TagValue in the following way: 33// 34// - The string that underlies TagValue is scanned byte by byte. 35// 36// - If a byte represents a legal OpenTSDB rune with the exception of '_', that 37// byte is directly copied to the resulting JSON byte slice. 38// 39// - If '_' is encountered, it is replaced by '__'. 40// 41// - If ':' is encountered, it is replaced by '_.'. 42// 43// - All other bytes are replaced by '_' followed by two bytes containing the 44// uppercase ASCII representation of their hexadecimal value. 45// 46// This encoding allows to save arbitrary Go strings in OpenTSDB. That's 47// required because Prometheus label values can contain anything, and even 48// Prometheus metric names may (and often do) contain ':' (which is disallowed 49// in OpenTSDB strings). The encoding uses '_' as an escape character and 50// renders a ':' more or less recognizable as '_.' 51// 52// Examples: 53// 54// "foo-bar-42" -> "foo-bar-42" 55// 56// "foo_bar_42" -> "foo__bar__42" 57// 58// "http://example.org:8080" -> "http_.//example.org_.8080" 59// 60// "Björn's email: bjoern@soundcloud.com" -> 61// "Bj_C3_B6rn_27s_20email_._20bjoern_40soundcloud.com" 62// 63// "日" -> "_E6_97_A5" 64func (tv TagValue) MarshalJSON() ([]byte, error) { 65 length := len(tv) 66 // Need at least two more bytes than in tv. 67 result := bytes.NewBuffer(make([]byte, 0, length+2)) 68 result.WriteByte('"') 69 for i := 0; i < length; i++ { 70 b := tv[i] 71 switch { 72 case (b >= '-' && b <= '9') || // '-', '.', '/', 0-9 73 (b >= 'A' && b <= 'Z') || 74 (b >= 'a' && b <= 'z'): 75 result.WriteByte(b) 76 case b == '_': 77 result.WriteString("__") 78 case b == ':': 79 result.WriteString("_.") 80 default: 81 result.WriteString(fmt.Sprintf("_%X", b)) 82 } 83 } 84 result.WriteByte('"') 85 return result.Bytes(), nil 86} 87 88// UnmarshalJSON unmarshals JSON strings coming from OpenTSDB into Go strings 89// by applying the inverse of what is described for the MarshalJSON method. 90func (tv *TagValue) UnmarshalJSON(json []byte) error { 91 escapeLevel := 0 // How many bytes after '_'. 92 var parsedByte byte 93 94 // Might need fewer bytes, but let's avoid realloc. 95 result := bytes.NewBuffer(make([]byte, 0, len(json)-2)) 96 97 for i, b := range json { 98 if i == 0 { 99 if b != '"' { 100 return fmt.Errorf("expected '\"', got %q", b) 101 } 102 continue 103 } 104 if i == len(json)-1 { 105 if b != '"' { 106 return fmt.Errorf("expected '\"', got %q", b) 107 } 108 break 109 } 110 switch escapeLevel { 111 case 0: 112 if b == '_' { 113 escapeLevel = 1 114 continue 115 } 116 result.WriteByte(b) 117 case 1: 118 switch { 119 case b == '_': 120 result.WriteByte('_') 121 escapeLevel = 0 122 case b == '.': 123 result.WriteByte(':') 124 escapeLevel = 0 125 case b >= '0' && b <= '9': 126 parsedByte = (b - 48) << 4 127 escapeLevel = 2 128 case b >= 'A' && b <= 'F': // A-F 129 parsedByte = (b - 55) << 4 130 escapeLevel = 2 131 default: 132 return fmt.Errorf( 133 "illegal escape sequence at byte %d (%c)", 134 i, b, 135 ) 136 } 137 case 2: 138 switch { 139 case b >= '0' && b <= '9': 140 parsedByte += b - 48 141 case b >= 'A' && b <= 'F': // A-F 142 parsedByte += b - 55 143 default: 144 return fmt.Errorf( 145 "illegal escape sequence at byte %d (%c)", 146 i, b, 147 ) 148 } 149 result.WriteByte(parsedByte) 150 escapeLevel = 0 151 default: 152 panic("unexpected escape level") 153 } 154 } 155 *tv = TagValue(result.String()) 156 return nil 157} 158