summaryrefslogtreecommitdiff
path: root/src/Database/InfluxDB/Line.hs
blob: af88ec13eb71bf0b4359e3fefec212edf9b75554 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Database.InfluxDB.Line
  ( -- $setup

  -- * Types and accessors
    Line(Line)
  , measurement
  , tagSet
  , fieldSet
  , timestamp

  -- * Serializers
  , buildLine
  , buildLines
  , encodeLine
  , encodeLines

  -- * Other types
  , LineField
  , Field(..)
  , Precision(..)
  ) where
import Data.List (intersperse)
import Data.Int (Int64)
import Data.Monoid
import Prelude

import Control.Lens
import Data.Map (Map)
import Data.Text (Text)
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Map.Strict as Map
import qualified Data.Text.Encoding as TE

import Database.InfluxDB.Internal.Text
import Database.InfluxDB.Types

{- $setup
The Line protocol implementation.

>>> :set -XOverloadedStrings
>>> import Database.InfluxDB
>>> import Data.Time
>>> import qualified Data.ByteString.Lazy.Char8 as BL8
>>> import System.IO (stdout)
>>> :{
let l1 = Line "cpu_usage"
      (Map.singleton "cpu" "cpu-total")
      (Map.fromList
        [ ("idle",   FieldFloat 10.1)
        , ("system", FieldFloat 53.3)
        , ("user",   FieldFloat 46.6)
        ])
      (Just $ parseTimeOrError False defaultTimeLocale
        "%F %T%Q %Z"
        "2017-06-17 15:41:40.42659044 UTC") :: Line UTCTime
:}
-}

-- | Placeholder for the Line Protocol
--
-- See https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/ for the
-- concrete syntax.
data Line time = Line
  { _measurement :: !Measurement
  -- ^ Measurement name
  , _tagSet :: !(Map Key Key)
  -- ^ Set of tags (optional)
  , _fieldSet :: !(Map Key LineField)
  -- ^ Set of fields
  --
  -- It shouldn't be empty.
  , _timestamp :: !(Maybe time)
  -- ^ Timestamp (optional)
  }

-- | Serialize a 'Line' to a lazy bytestring
--
-- >>> BL8.putStrLn $ encodeLine (scaleTo Second) l1
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
encodeLine
  :: (time -> Int64)
  -- ^ Function to convert time to an InfluxDB timestamp
  --
  -- Use 'scaleTo' for HTTP writes and 'roundTo' for UDP writes.
  -> Line time
  -> L.ByteString
encodeLine toTimestamp = B.toLazyByteString . buildLine toTimestamp

-- | Serialize 'Line's to a lazy bytestring
--
-- >>> BL8.putStr $ encodeLines (scaleTo Second) [l1, l1]
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
--
encodeLines
  :: Foldable f
  => (time -> Int64)
  -- ^ Function to convert time to an InfluxDB timestamp
  --
  -- Use 'scaleTo' for HTTP writes and 'roundTo' for UDP writes.
  -> f (Line time)
  -> L.ByteString
encodeLines toTimestamp = B.toLazyByteString . buildLines toTimestamp

-- | Serialize a 'Line' to a bytestring 'B.Buider'
--
-- >>> B.hPutBuilder stdout $ buildLine (scaleTo Second) l1
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
buildLine
  :: (time -> Int64)
  -> Line time
  -> B.Builder
buildLine toTimestamp Line {..} =
  key <> " " <> fields <> maybe "" (" " <>) timestamp
  where
    measurement = TE.encodeUtf8Builder $ escapeMeasurement _measurement
    tags = buildMap (TE.encodeUtf8Builder . escapeKey) _tagSet
    key = if Map.null _tagSet
      then measurement
      else measurement <> "," <> tags
    fields = buildMap buildFieldValue _fieldSet
    timestamp = B.int64Dec . toTimestamp <$> _timestamp
    buildMap encodeVal =
      mconcat . intersperse "," . map encodeKeyVal . Map.toList
      where
        encodeKeyVal (name, val) = mconcat
          [ TE.encodeUtf8Builder $ escapeKey name
          , "="
          , encodeVal val
          ]

escapeKey :: Key -> Text
escapeKey (Key text) = escapeCommas $ escapeEqualSigns $ escapeSpaces text

escapeMeasurement :: Measurement -> Text
escapeMeasurement (Measurement text) = escapeCommas $ escapeSpaces text

escapeStringField :: Text -> Text
escapeStringField = escapeDoubleQuotes

buildFieldValue :: LineField -> B.Builder
buildFieldValue = \case
  FieldInt i -> B.int64Dec i <> "i"
  FieldFloat d -> B.doubleDec d
  FieldString t -> "\"" <> TE.encodeUtf8Builder (escapeStringField t) <> "\""
  FieldBool b -> if b then "true" else "false"

-- | Serialize 'Line's to a bytestring 'B.Builder'
--
-- >>> B.hPutBuilder stdout $ buildLines (scaleTo Second) [l1, l1]
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
-- cpu_usage,cpu=cpu-total idle=10.1,system=53.3,user=46.6 1497714100
--
buildLines
  :: Foldable f
  => (time -> Int64)
  -> f (Line time)
  -> B.Builder
buildLines toTimestamp = foldMap ((<> "\n") . buildLine toTimestamp)

makeLensesWith (lensRules & generateSignatures .~ False) ''Line

-- | Name of the measurement that you want to write your data to.
measurement :: Lens' (Line time) Measurement

-- | Tag(s) that you want to include with your data point. Tags are optional in
-- the Line Protocol, so you can set it 'empty'.
tagSet :: Lens' (Line time) (Map Key Key)

-- | Field(s) for your data point. Every data point requires at least one field
-- in the Line Protocol, so it shouldn't be 'empty'.
fieldSet :: Lens' (Line time) (Map Key LineField)

-- | Timestamp for your data point. You can put whatever type of timestamp that
-- is an instance of the 'Timestamp' class.
timestamp :: Lens' (Line time) (Maybe time)