# drain.conf ############# ## MODULES ## ############# module( load="impstats" interval="60" format="json" ruleset="rs_stats" ) module( load="imhttp" ports="10454" liboptions=[ "access_log_file=/dev/null", "error_log_file=/dev/stderr", "num_threads=50", "listen_backlog=32", "connection_queue=16" ] documentroot="/www/data" ) module(load="mmfields") module(load="mmjsonparse") module(load="mmnormalize") module(load="omprog") ################### ## LOOKUP TABLES ## ################### # Heroku metadata lookup tables. lookup_table(name="heroku_apps" file="/etc/rsyslog.d/tables/heroku-apps.json") lookup_table(name="heroku_spaces" file="/etc/rsyslog.d/tables/heroku-spaces.json") lookup_table(name="heroku_regions" file="/etc/rsyslog.d/tables/heroku-regions.json") lookup_table(name="heroku_addons" file="/etc/rsyslog.d/tables/heroku-addons.json") ############## ## COUNTERS ## ############## # General counters. dyn_stats(name="heroku" resettable="off") # Number of events this drain has seen counted by type. dyn_stats(name="drain.events.total" resettable="off") # Number of app events counted per source and type. dyn_stats(name="app.events.total" resettable="off") # Total of router request events. dyn_stats(name="router.requests.total" resettable="off") ############ ## INPUTS ## ############ input( type="imhttp" name="drain" endpoint="/v1/logs/41579b26-d76b-4f3f-bcf8-e51ba6942879" basicAuthFile="/dev/shm/auth/htpasswd" ruleset="rs_drain" addmetadata="on" SupportOctetCountedFraming="on" ) ############### ## TEMPLATES ## ############### # Event timestamp. template(name="tpl_timestamp_rfc3339" type="list") { property(name="timestamp" dateFormat="rfc3339") } # App event counter tag template. # Format: "||||||||". # e.g. "bbd9f128-380d-4de9-af4b-4d62b44a8dfd|app|web.1|log". template(name="tpl_heroku_app_event_tags" type="list") { property(name="$.app_id") constant(value="|") # App ID or Drain Token. property(name="$.source") constant(value="|") # Source. property(name="$.dyno") constant(value="|") # Dyno. property(name="$.type") constant(value="|") # Event type. property(name="$.app") constant(value="|") # App property(name="$.space") constant(value="|") # Space property(name="$.region") constant(value="|") # Region property(name="$.team") constant(value="|") # Team property(name="$.service") # Service } # Router request counter tag template. # Format: "||||||||||". # e.g. "bbd9f128-380d-4de9-af4b-4d62b44a8dfd|web.1|POST|log-drain.herokuapp.com|200|-". # e.g. "bbd9f128-380d-4de9-af4b-4d62b44a8dfd|-|POST|log-drain.herokuapp.com|503|H10". template(name="tpl_heroku_router_request_tags" type="list") { property(name="$.app_id") constant(value="|") # App ID or Drain Token. property(name="$.dyno") constant(value="|") # Dyno. property(name="$.method") constant(value="|") # HTTP method property(name="$.host") constant(value="|") # HTTP host. property(name="$.status") constant(value="|") # HTTP status. property(name="$.code") constant(value="|") # Heroku error code. property(name="$.app") constant(value="|") # App property(name="$.space") constant(value="|") # Space property(name="$.region") constant(value="|") # Region property(name="$.team") constant(value="|") # Team property(name="$.service") # Service } # template for impstats_to_json plugin template(name="tpl_impstats_json_record" type="list") { property(name="$!stats") constant(value="\n") } ########################## ## RULESET COUNT EVENTS ## ########################## # Increment event counters. Caller should set the required variables. # - $.type # - $.source # - $.dyno # - $.app_id ruleset(name="rs_count_events") { set $.inc = dyn_inc("drain.events.total", $.type); set $.inc = dyn_inc("app.events.total", exec_template("tpl_heroku_app_event_tags")); } ################### ## RULESET DRAIN ## ################### # Tee events to the correct ruleset based on the event type. ruleset( name="rs_drain" queue.size="750000" queue.dequeueBatchSize="6000" queue.filename="rs_drain_queue" queue.spoolDirectory="/tmp" queue.maxFileSize="500m" ) { unset $!metadata!httpheaders!authorization; unset $!metadata!queryparams!mirror; # Guard against badly parsed events so we don't produce garbage values. if ($app-name != ["app", "heroku", "token"]) then { set $.inc = dyn_inc("heroku", "invalid.events.total"); set $.app_id = "-"; set $.source = "-"; set $.dyno = "-"; } else { # If HTTP Header "Logplex-Drain-Token" exists, then logs are coming from Logplex. # In this case, use the Drain Token as the unique App ID. # Otherwise, we're looking at logs from a Space level drain, where the App ID is the syslog hostname. if (strlen($!metadata!httpheaders!logplex-drain-token) > 0) then { set $.app_id = $!metadata!httpheaders!logplex-drain-token; } else { set $.app_id = $hostname; } set $.source = $app-name; set $.dyno = $procid; } # set metadata info for msg set $.app = lookup("heroku_apps", $.app_id); set $.space = lookup("heroku_spaces", $.app_id); set $.region = lookup("heroku_regions", $.app_id); set $.team = "-"; set $.service = "-"; set $.sourcetype = "-"; set $.environment = "testing"; set $.cloud = "digitalengagement"; set $.business_unit = "Heroku"; # override with query param if they exist, fix-up encoded space characters if ($!metadata!queryparams!app != "") then { reset $.app = replace($!metadata!queryparams!app, "%20", " "); } if ($!metadata!queryparams!space != "") then { reset $.space = replace($!metadata!queryparams!space, "%20", " "); } if ($!metadata!queryparams!region != "") then { reset $.region = replace($!metadata!queryparams!region, "%20", " "); } if ($!metadata!queryparams!service != "") then { reset $.service = replace($!metadata!queryparams!service, "%20", " "); } if ($!metadata!queryparams!team != "") then { reset $.team = replace($!metadata!queryparams!team, "%20", " "); } if ($!metadata!queryparams!sourcetype != "") then { reset $.sourcetype = replace($!metadata!queryparams!sourcetype, "%20", " "); } if ($!metadata!queryparams!cloud != "") then { reset $.cloud = replace($!metadata!queryparams!cloud, "%20", " "); } if ($!metadata!queryparams!environment != "") then { reset $.environment = replace($!metadata!queryparams!environment, "%20", " "); } if ($!metadata!queryparams!bu != "") then { reset $.business_unit = replace($!metadata!queryparams!bu, "%20", " "); } if ($msg startswith "dyno=" or $msg startswith "source=") then { call rs_runtime_metrics } call rs_logs if ($.dyno == "router") then { call rs_router_metrics } call rs_debug_files } ######################### ## RULESET DEBUG FILES ## ######################### ruleset(name="rs_debug_files") { set $.dynadir = "original"; action( type="omfile" dynafile="tpl_dynafile" ) set $.dynadir = "debug"; action( type="omfile" template="RSYSLOG_DebugFormat" dynafile="tpl_dynafile" ) } ############################ ## RULESET ROUTER METRICS ## ############################ # Parse Heroku router logs as metrics and increment internal counters. ruleset(name="rs_router_metrics" queue.size="1000") { # Possible fields from various router logs. # at=info # method=GET # path="/" # host=app.herokuapp.com # request_id= # fwd="" # dyno=web.1 # connect=0ms # service=3ms # status=304 # bytes=0 # protocol=http # tls_version=tls1.3 # code=H10 # desc="App crashed" action(type="mmfields" separator=" " jsonRoot=".fields") foreach ($.field in $.fields) do { if ($.field!value startswith "method=") then { set $.method = field($.field!value, 61, 2); } else if ($.field!value startswith "host=") then { set $.host = field($.field!value, 61, 2); } else if ($.field!value startswith "dyno=") then { set $.dyno = field($.field!value, 61, 2); } else if ($.field!value startswith "status=") then { set $.status = field($.field!value, 61, 2); } else if ($.field!value startswith "code=") then { set $.code = field($.field!value, 61, 2); } } # The dyno field is empty under some errors conditions, default it. if (strlen($.dyno) == 0) then { set $.dyno = "-"; } # The code field is empty if the event is not an error, default it. if (strlen($.code) == 0) then { set $.code = "-"; } set $.request_tags = exec_template("tpl_heroku_router_request_tags"); set $.inc = dyn_inc("router.requests.total", $.request_tags); set $.dynadir = "debug-router-events"; action( type="omfile" template="RSYSLOG_DebugFormat" dynafile="tpl_dynafile" ) } ############################# ## RULESET RUNTIME METRICS ## ############################# # Parse and publish Heroku runtime-metrics. ruleset(name="rs_runtime_metrics") { # Parse a few different possible metric sources using mmnormalize. # Addon: # source= addon= ... # source=KAFKA addon=kafka-opaque-17963 sample#load-avg-1m=0 sample#load-avg-5m=0 ... # Dyno: # dyno= source= ... Note that the dyno here is *not* the actual Dyno name, rather a few ID's. # source= dyno= ... Or reversed. Match with prefix in mmnormalize. # dyno=heroku.c273506e-9ddc-47b6-a40f-b15f6b94c828.c8855b1b-c6d9-4304-865e-5d379989948c source=web.1 sample#memory_total=265.59MB ... # source=web.1 dyno=heroku.c273506e-9ddc-47b6-a40f-b15f6b94c828.c8855b1b-c6d9-4304-865e-5d379989948c sample#memory_total=265.59MB ... action( type="mmnormalize" path="$.runtime" rule=[ "prefix=source=%source:word% ", "rule=:addon=%addon:word% %rest:rest%", "rule=:dyno=%dyno:word% %rest:rest%", "prefix=", "rule=:dyno=%dyno:word% source=%source:word% %rest:rest%" ] ) if (strlen($.runtime!addon) > 0) then { # If the addons table is populated, try to find the associated app. set $.app_id = lookup("heroku_addons", $.runtime!addon); if ($.app_id == "-") then { set $.app_id = $hostname; } # Additional tags for addon metrics. # The addon field reflects the addon instance name. # The source field reflects the attachment instance name. set $!metric_tags!addon = $.runtime!addon; set $!metric_tags!attachment = $.runtime!source; # Addons postgres, redis, and kafka get their own metric heirarchy. # Postgres and redis can be identified by their dyno name exactly. # Kafka dyno names are "heroku-kafka.". if ($.dyno == ["heroku-postgres", "heroku-redis"]) then { set $!metric_service = $.dyno; } else if ($.dyno startswith "heroku-kafka") then { set $!metric_service = "heroku-kafka"; } else { # Catch-all for unknown addons or formats. set $!metric_service = "heroku-addon"; } } else { # Extract the app id from the dyno field in the runtime-metric, e.g. "dyno=heroku..". # We have the app id in the $hostname variable from private space apps, but not for common runtime apps. set $.app_id = field($.runtime!dyno, 46, 2); # 2nd field by ascii 46 "." set $!metric_service = "heroku-runtime"; } # now that the addon's app_id is set, try to retrieve any missed metadata if ($.app == "-") then { reset $.app = lookup("heroku_apps", $.app_id); } if ($.space == "-") then { reset $.space = lookup("heroku_spaces", $.app_id); } if ($.region == "-") then { reset $.region = lookup("heroku_regions", $.app_id); } # Count once we've resolved the app id. set $.type = "metric"; call rs_count_events # Timestamp. set $!metric_timestamp = parse_time($timestamp); # Tags. set $!metric_tags!source = $.source; set $!metric_tags!dyno = $.dyno; set $!metric_tags!device = $.app_id; set $!metric_tags!app = $.app; set $!metric_tags!space = $.space; set $!metric_tags!region = $.region; set $!metric_tags!uuid = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; set $!metric_tags!team = $.team; set $!metric_tags!service = $.service; # Parse the message into space-separated fields using mmfields. # { "f1": "source=KAFKA", "f2": "addon=kafka-opaque-17963", "f3": "sample#load-avg-1m=0", ... } action(type="mmfields" separator=" " jsonRoot=".fields") foreach ($.field in $.fields) do { if ($.field!value startswith "sample#") then { set $.sample = field($.field!value, 35, 2); # 2nd field by ascii 35 "#" set $.sample_key = field($.sample, 61, 1); # 1st field by ascii 61 "=" set $.sample_value = field($.sample, 61, 2); # 2nd field by ascii 61 "=" # Regex parses integers, floats, and units. # 0.53591 -> (0.53591) # 0 -> (0) # 10528428kB -> (10528428, kB) set $.value = re_extract($.sample_value, "([[:digit:]]+(\\.[[:digit:]]+)?)([[:alpha:]]+)?", 0, 1, "0"); # Full match (group 0) subgroup 1 set $.units = re_extract($.sample_value, "([[:digit:]]+(\\.[[:digit:]]+)?)([[:alpha:]]+)?", 0, 3, ""); # Full match (group 0) subgroup 3 set $!metric_value = $.value; if (strlen($.units) > 0) then { set $!metric_name = $.sample_key & "." & $.units; } else { set $!metric_name = $.sample_key; } set $.dynadir = "debug-runtime-metrics"; action( type="omfile" template="RSYSLOG_DebugFormat" dynafile="tpl_dynafile" ) call rs_funnel_metrics } } } ################## ## RULESET LOGS ## ################## # Parse and publish Heroku logs. ruleset(name="rs_logs") { # Count. set $.type = "log"; call rs_count_events # Build fields for event-flatten v2 set $!data!event = $msg; set $.format = "text"; # Try to parse $msg as JSON. if ($msg startswith "{") then { action(type="mmjsonparse" cookie="" container="$.jsonmsg") if ($parsesuccess == "OK") then { reset $!data!event = $.jsonmsg; reset $.format = "json"; } } # Format source field as "heroku." to identify logs coming from Heroku in Splunk. # e.g. "heroku.heroku" for platform/router logs, "heroku.app" for app logs, and "heroku.token" for user API actions. set $!data!source = "heroku." & $.source; set $!data!sourcetype = $.sourcetype; # No sourcetype currently. set $!data!hostname = $.app_id; # App id, "host", or "heroku", depending on the type of app. set $!data!agent_timestamp = exec_template("tpl_timestamp_rfc3339"); # Tags set $!data!tags_schema_id = "any6:1"; set $!data!tags!dyno = $.dyno; # The dyno name, e.g. "web.1" set $!data!tags!app = $.app; set $!data!tags!space = $.space; set $!data!tags!region = $.region; set $!data!tags!severity = $syslogseverity-text; set $!data!tags!uuid = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; # Owner set $!data!owner!service = $.service; set $!data!owner!team = $.team; set $!data!owner!environment = $.environment; set $!data!owner!cloud = $.cloud; # Filter DNR logs from a few sources: # - Heroku system and API logs. # - JSON formatted app logs with "logRecordType=(Authentication|Verification)". # - App logs matching a user-provided filter expression. set $.dnr = "false"; if ($.source == "heroku") or ($.source == "app" and $.dyno == "api") then { reset $.dnr = "true"; } if ($.format == "json") and (tolower($.jsonmsg!logRecordType) == ["authentication", "verification"]) then { reset $.dnr = "true"; } else if ($.format == "json") and ($.jsonmsg!logRecordType == "") then { unset $.jsonmsg!logRecordType; } if ($msg contains "JWT" or $msg contains "issuer:" or $msg contains "SalesforceBearerAuthTokenHandler") then { reset $.dnr = "true"; } if ($.dnr == "true") then { set $!data!facility = $syslogfacility-text; set $!data!priority = $syslogpriority-text; set $!data!uuid = $uuid; set $!data!dnr!environment = $.business_unit; call rs_funnel_dnr_logs } call rs_funnel_logs } ################### ## RULESET STATS ## ################### # Publish Heroku event counts and handle rsyslog internal stats/logs. ruleset(name="rs_stats" queue.size="1000") { set $.inc = dyn_inc("heroku", "metadata.info"); # Setting format="json" in impstats produces the following JSON, no @cee cookie. # { "name": "rs_demux", "origin": "core.queue", "size": 0, "enqueued": 1795, "full": 0, # "discarded.full": 0, "discarded.nf": 0, "maxqsize": 10 } action( type="mmjsonparse" name="action_stats_mmjsonparse" cookie="" container="$!stats" ) # push out impstats stream action( type="omprog" name="action_impstats_to_json_script" template="tpl_impstats_json_record" binary="/usr/share/rsyslog/plugins/impstats_to_json.py --log-file=stdout --impstats-file=/tmp/impstats.json --save-interval-secs=120 --allow-list=main_Q,resource_usage,omhttp,action_omhttp_funnel_logs,action_omhttp_funnel_dnr_logs,action_omhttp_funnel_metrics" queue.type="LinkedList" queue.saveOnShutdown="off" queue.workerThreads="1" action.resumeInterval="5" killUnresponsive="on" forceSingleInstance="on" ) # Log these stats events. if ($!stats!origin == ["core.queue", "core.action", "impstats", "imhttp", "omhttp", "dynstats.bucket", "dynstats"]) then { call rs_logger } # Argus service. set $!metric_service = "heroku-drain"; # Timestamp. set $!metric_timestamp = parse_time($timestamp); set $.app_id = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; set $.source = "app"; set $.dyno = "web.1"; # Drain tags. set $!metric_tags!source = $.source; set $!metric_tags!dyno = $.dyno; set $!metric_tags!device = $.app_id; set $!metric_tags!app = "sfdc-lm-test-vir-00-sh-drain"; set $!metric_tags!space = lookup("heroku_spaces", $.app_id); set $!metric_tags!region = lookup("heroku_regions", $.app_id); set $!metric_tags!uuid = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; # Parsing either dynstats bucket or plugin. # bucket: { "name": "heroku", "origin": "dynstats.bucket", "values": { "metadata.info": 16 } } # imhttp: { "name": "imhttp", "origin": "imhttp", "submitted": 26, "failed": 0, "discarded": 0 } # impstats: { "name": "resource-usage", "origin": "impstats", "utime": 32000, "stime": 44000, "maxrss": 11948, # "minflt": 2232, "majflt": 0, "inblock": 0, "oublock": 1360, "nvcsw": 2514, "nivcsw": 135, "openfiles": 14 } if ($!stats!origin == "dynstats.bucket") then { foreach ($.bucket in $!stats!values) do { if ($!stats!name == "app.events.total") then { # Set a tag that is unique within our app id (uuid) tag. # Allows multiple drain dynos to produce metrics about the same app dyno. set $!metric_tags!drain_dyno = "web.1"; set $.event_app_id = field($.bucket!key, 124, 1); # Event tags. reset $!metric_tags!source = field($.bucket!key, 124, 2); reset $!metric_tags!dyno = field($.bucket!key, 124, 3); reset $!metric_tags!type = field($.bucket!key, 124, 4); reset $!metric_tags!device = $.event_app_id; reset $!metric_tags!app = lookup("heroku_apps", $.event_app_id); reset $!metric_tags!space = lookup("heroku_spaces", $.event_app_id); reset $!metric_tags!region = lookup("heroku_regions", $.event_app_id); # override if exist in bucket!key set $._app = field($.bucket!key, 124, 5); if $._app != "-" then { reset $!metric_tags!app = $._app; } set $._space = field($.bucket!key, 124, 6); if $._space != "-" then { reset $!metric_tags!space = $._space; } set $._team = field($.bucket!key, 124, 8); if $._team != "-" then { reset $!metric_tags!team = $._team; } set $._service = field($.bucket!key, 124, 9); if $._service != "-" then { reset $!metric_tags!service = $._service; } # Metric: "heroku.app.events.total=". set $!metric_name = "heroku." & $!stats!name; set $!metric_value = $.bucket!value; } else if ($!stats!name == "drain.events.total") then { reset $!metric_tags!type = $.bucket!key; # Metric: "heroku.drain.events.total=". set $!metric_name = "heroku." & $!stats!name; set $!metric_value = $.bucket!value; } else if ($!stats!name == "router.requests.total") then { # Set drain dyno tag. set $!metric_tags!drain_dyno = "web.1"; set $.event_app_id = field($.bucket!key, 124, 1); # Router event tags. reset $!metric_tags!source = "heroku"; reset $!metric_tags!dyno = field($.bucket!key, 124, 2); set $!metric_tags!method = field($.bucket!key, 124, 3); set $!metric_tags!host = field($.bucket!key, 124, 4); set $!metric_tags!status = field($.bucket!key, 124, 5); set $!metric_tags!code = field($.bucket!key, 124, 6); reset $!metric_tags!device = $.event_app_id; reset $!metric_tags!app = lookup("heroku_apps", $.event_app_id); reset $!metric_tags!space = lookup("heroku_spaces", $.event_app_id); reset $!metric_tags!region = lookup("heroku_regions", $.event_app_id); # override if exist in bucket!key set $._app = field($.bucket!key, 124, 7); if $._app != "-" then { reset $!metric_tags!app = $._app; } set $._space = field($.bucket!key, 124, 8); if $._space != "-" then { reset $!metric_tags!space = $._space; } set $._region = field($.bucket!key, 124, 9); if $._region != "-" then { reset $!metric_tags!region = $._region; } set $._team = field($.bucket!key, 124, 10); if $._team != "-" then { reset $!metric_tags!team = $._team; } set $._service = field($.bucket!key, 124, 11); if $._service != "-" then { reset $!metric_tags!service = $._service; } # Metric: "heroku.router.requests.total=". set $!metric_name = "heroku." & $!stats!name; set $!metric_value = $.bucket!value; } else if ($!stats!name == "heroku" and $.bucket!key == "metadata.info") then { set $!metric_tags!release = "v77"; set $!metric_tags!version = "v0.0.9"; set $!metric_tags!build = "33381b4a2a446016e5d3829768e1b4c6fd12b9a3"; # Metric: "heroku.metadata.info=1". set $!metric_name = "heroku." & $.bucket!key; set $!metric_value = 1; } else { stop } set $.dynadir = "debug-event-metrics"; action( type="omfile" template="RSYSLOG_DebugFormat" dynafile="tpl_dynafile" ) call rs_funnel_metrics } } if ($!stats!origin == ["imhttp", "impstats", "omhttp", "core.action", "core.queue"]) then { foreach ($.field in $!stats) do { if ($.field!key != ["name", "origin"]) then { # Replace "-" -> "_" for consistency with other rsyslog metrics. set $!metric_name = "rsyslog.impstats." & replace($!stats!name, "-", "_") & "." & $.field!key; set $!metric_value = $.field!value; # Do tag customizations by adding `statsname` as action tag # for the following origins: if ($!stats!origin == "omhttp" or $!stats!origin == "core.action") then { reset $!metric_name = "rsyslog.impstats." & $!stats!origin & "." & $.field!key; set $!metric_tags!action = $!stats!name; } else if ($!stats!origin == "core.queue") then { # customize for `core.queue` metric reset $!metric_name = "rsyslog.impstats." & $!stats!origin & "." & $.field!key; set $!metric_tags!queue = $!stats!name; } set $.dynadir = "debug-self-metrics"; action( type="omfile" template="RSYSLOG_DebugFormat" dynafile="tpl_dynafile" ) call rs_funnel_metrics } } } } ruleset(name="rs_logger") { # setup tpl_event_flatten_v2 envelope for rsyslog internal logs set $!data!tags_schema_id = "any6:1"; set $!data!tags!dyno = "web.1"; set $!data!tags!app = "sfdc-lm-test-vir-00-sh-drain"; set $!data!source = "heroku.app"; set $!data!sourcetype = "collection:rsyslog"; set $!data!hostname = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; set $!data!agent_timestamp = exec_template("tpl_timestamp_rfc3339"); set $!data!tags!space = lookup("heroku_spaces", $!data!hostname); set $!data!tags!region = lookup("heroku_regions", $!data!hostname); set $!data!tags!severity = $syslogseverity-text; set $!data!tags!uuid = "41579b26-d76b-4f3f-bcf8-e51ba6942879"; set $!data!owner!service = "collection"; set $!data!owner!team = "collection"; set $!data!owner!environment = "testing"; set $!data!owner!cloud = "moncloud"; set $!data!event = $msg; set $.format = "text"; if ($msg startswith "{") then { action(type="mmjsonparse" cookie="" container="$.jsonmsg") if ($parsesuccess == "OK") then { reset $!data!event = $.jsonmsg; reset $.format = "json"; } } call rs_stdout call rs_funnel_logs } # Log anything that gets here, too. call rs_logger stop