influxdb-1.7.1.2: src/Database/InfluxDB/Line.hs
{-# 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 . escapeBackslashes
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 'Control.Applicative.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 'Control.Applicative.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)