How to log user activity in a streamlit app?
Asked Answered
S

1

5

I have a streamlit app that is public (ie. no user log-in). I would like to have log files of the form:

|2023-02-10 16:30:16 : user at ip=___ clicked button key=___
|2023-02-10 16:30:19 : user at ip=___ clicked button key=___
|2023-02-10 16:31:10 : user at ip=___ clicked button key=___
|2023-02-10 16:31:27 : user at ip=___ clicked button key=___
|...

Is there any way to achieve this? It's because I want to do some analytics on how the app is being used.

Slyviasm answered 10/2, 2023 at 10:48 Comment(0)
B
8

You can access the remote ip address via get_script_run_ctx and .remote_ip:

from streamlit import runtime
from streamlit.runtime.scriptrunner import get_script_run_ctx


def get_remote_ip() -> str:
    """Get remote ip."""

    try:
        ctx = get_script_run_ctx()
        if ctx is None:
            return None

        session_info = runtime.get_instance().get_client(ctx.session_id)
        if session_info is None:
            return None
    except Exception as e:
        return None

    return session_info.request.remote_ip


import streamlit as st

st.title("Title")
st.markdown(f"The remote ip is {get_remote_ip()}")

For the logging part, I suggest you use a ContextFilter:

import logging

class ContextFilter(logging.Filter):
    def filter(self, record):
        record.user_ip = get_remote_ip()
        return super().filter(record)

This custom filter will modify the LogRecord and add it the custom attribute user_ip that you can then use inside the Formatter.

All together, it gives:

import logging

import streamlit as st
from streamlit import runtime
from streamlit.runtime.scriptrunner import get_script_run_ctx

def get_remote_ip() -> str:
    """Get remote ip."""

    try:
        ctx = get_script_run_ctx()
        if ctx is None:
            return None

        session_info = runtime.get_instance().get_client(ctx.session_id)
        if session_info is None:
            return None
    except Exception as e:
        return None

    return session_info.request.remote_ip

class ContextFilter(logging.Filter):
    def filter(self, record):
        record.user_ip = get_remote_ip()
        return super().filter(record)

def init_logging():
    # Make sure to instanciate the logger only once
    # otherwise, it will create a StreamHandler at every run
    # and duplicate the messages

    # create a custom logger
    logger = logging.getLogger("foobar")
    if logger.handlers:  # logger is already setup, don't setup again
        return
    logger.propagate = False
    logger.setLevel(logging.INFO)
    # in the formatter, use the variable "user_ip"
    formatter = logging.Formatter("%(name)s %(asctime)s %(levelname)s [user_ip=%(user_ip)s] - %(message)s")
    handler = logging.StreamHandler()
    handler.setLevel(logging.INFO)
    handler.addFilter(ContextFilter())
    handler.setFormatter(formatter)
    logger.addHandler(handler)

def main():
    logger.info("Inside main")
    st.title("Title")

    text = st.sidebar.text_input("Text:")
    logger.info(f"This is the text: {text}")

if __name__ == "__main__":
    init_logging()

    logger = logging.getLogger("foobar")
    main()
foobar 2023-02-13 15:43:57,252 INFO [user_ip=::1] - Inside main
foobar 2023-02-13 15:43:57,253 INFO [user_ip=::1] - This is the text: Hello, world!

Note: Here the user_ip is "::1" because everything was done locally.

Beckmann answered 13/2, 2023 at 14:46 Comment(2)
get_remote_ip() seems to be returning my remote server's IP address.Dextroglucose
As with M.Viking above, this seems to return the server's IP address. I tested with two different clients on two different networks. Test URL: cover-this-sessions-test.streamlit.appPallmall

© 2022 - 2024 — McMap. All rights reserved.