{-# 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)